mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
perf(plugins): share host code identity for bundled TypeScript plugins (#163639)
Bundled plugins only shared the host loader when their entry was compiled JavaScript, so a source checkout captured all 87 bundled packages (copy, hash, and dependency materialization) on every prepared model runtime publication: 79.5 s of plugin discovery per agent turn, which exceeded the 120 s budget on a loaded host. Bundled plugins now take the shared host loader regardless of entry extension, the edited-code warning keys on the canonical export so it survives loader interop wrappers, and the docs describe source and compiled bundled identity together. Discovery for the live embedded-runner case fell from 79.5 s to 12-13 s and the case from ~100 s to ~25 s on the same host. Editing a bundled plugin's TypeScript source now needs a Gateway restart, matching compiled bundled code; installed, external, workspace-path, and standalone plugins keep captured source reload semantics.
This commit is contained in:
parent
f8015dfe3f
commit
b5a41c2001
8 changed files with 192 additions and 172 deletions
|
|
@ -185,11 +185,13 @@ The receipt can also include cleanup warnings. Modules and native libraries may
|
|||
remain loaded after their registrations are removed.
|
||||
|
||||
Bundled plugins can reload while preserving their enabled or disabled policy.
|
||||
Compiled bundled plugins reuse their process-loaded code when their registrations
|
||||
reload. If the plugin's files changed while its original module remains loaded,
|
||||
Bundled plugins, including TypeScript source entries, reuse their process-loaded
|
||||
code when their registrations reload. If the plugin's files changed while its
|
||||
original module remains loaded,
|
||||
the result reports `restartRequired: true` with a warning, and CLI and tool
|
||||
output explain that a Gateway restart is needed to load edited code. Reload does
|
||||
not rebuild compiled bundled code; source installations also need a build.
|
||||
not rebuild bundled code. Rebuild before restarting when the installation loads
|
||||
compiled output.
|
||||
External captured sources return `restartRequired: false` after replacement.
|
||||
Reloading unchanged bundled files also returns `restartRequired: false`; channel
|
||||
and service registrations can be replaced without restarting the Gateway.
|
||||
|
|
|
|||
|
|
@ -216,13 +216,14 @@ unchanged plugin preserves that proof; changing its selected runtime files
|
|||
invalidates it.
|
||||
|
||||
A managed runtime instance owns its module results, registered callables, and
|
||||
runtime-store slots. With Node's synchronous module hooks, it also owns a captured
|
||||
source artifact. Package plugins capture their package inputs when the instance
|
||||
is created. Standalone files capture their entry and statically known inputs
|
||||
without copying the surrounding workspace. Compiled bundled runtime and setup
|
||||
modules share the host's code identity; each inventory still owns its registered
|
||||
callbacks and cleanup. Replacing that compiled code requires a build and Gateway
|
||||
restart. Conditional package aliases retain their package metadata, and native
|
||||
runtime-store slots. Non-bundled instances also own a captured source artifact.
|
||||
Package plugins capture their package inputs when the instance is created.
|
||||
Standalone files capture their entry and statically known inputs
|
||||
without copying the surrounding workspace. Bundled runtime and setup modules,
|
||||
including TypeScript source entries, share the host's code identity; each inventory
|
||||
still owns its registered callbacks and cleanup. Loading edited bundled code requires
|
||||
a Gateway restart; rebuild first when the installation loads compiled output.
|
||||
Conditional package aliases retain their package metadata, and native
|
||||
Node conditions, including `module-sync`, select the target from that captured metadata.
|
||||
Source inspection uses the same synchronous-module condition without evaluating plugin code.
|
||||
Captured source retains the difference between authored imports and require calls, so Bun's
|
||||
|
|
|
|||
|
|
@ -486,7 +486,7 @@ describe("capability loading from a Gateway generation", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("retains catalog factories within a cache generation and replaces them on source reload", () => {
|
||||
it("recreates bundled catalog providers while retaining process-loaded factory code", () => {
|
||||
withSpeechFixture((fixture) => {
|
||||
fixture.config.plugins = { enabled: true };
|
||||
const { pluginDir, runtimeImported } = declareCapabilityCatalog(fixture);
|
||||
|
|
@ -518,7 +518,7 @@ describe("capability loading from a Gateway generation", () => {
|
|||
withPluginRuntimeRegistryScope(current, () => {
|
||||
const replacement = speechProviders(fixture.config)[0];
|
||||
expect(replacement).not.toBe(first);
|
||||
expect(replacement?.label).toBe("replacement:catalog");
|
||||
expect(replacement?.label).toBe("source:catalog");
|
||||
});
|
||||
expect(fs.existsSync(runtimeImported)).toBe(false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import type { JitiOptions, JitiResolveOptions } from "jiti";
|
|||
import { isPathInside } from "../infra/path-guards.js";
|
||||
import { createJiti } from "./jiti-factory.js";
|
||||
import {
|
||||
isJavaScriptModulePath,
|
||||
resolvePluginLoaderTryNative,
|
||||
isPluginSourceModulePath,
|
||||
supportsBunRuntimeOnResolveTargets,
|
||||
|
|
@ -35,12 +34,11 @@ import { preparePluginLoaderAliases, isPluginSdkAliasSpecifier } from "./sdk-ali
|
|||
/** Runtime and setup share code identity policy while keeping separate instance authority. */
|
||||
export function bindPluginInstanceModuleLoader(params: PluginInstanceModuleLoaderParams): void {
|
||||
const cache = getPluginCache();
|
||||
if (params.origin === "bundled" && isJavaScriptModulePath(params.source)) {
|
||||
if (params.origin === "bundled") {
|
||||
if (params.expectedSourceDigest !== undefined) {
|
||||
throw new Error("Source digest validation is not applicable to core-bundled runtime modules");
|
||||
}
|
||||
// Core-shipped code keeps process identity. Recapturing it creates native ESM
|
||||
// module jobs that Node retains after the inventory and its callbacks retire.
|
||||
// Recaptured bundled code leaves native ESM jobs alive after its inventory retires.
|
||||
let loader: PluginModuleLoader;
|
||||
if (params.createHostModuleLoader) {
|
||||
loader = params.createHostModuleLoader();
|
||||
|
|
|
|||
|
|
@ -251,8 +251,46 @@ it("captures quiesced code without admitting calls and refuses capture after dis
|
|||
expect((restored.loadModule(entry) as FixtureModule).late()).toBe("original late import");
|
||||
});
|
||||
|
||||
it("shares bundled TypeScript code without captures and warns when edited code needs a restart", () => {
|
||||
const root = temp.make("bundled-source-identity-");
|
||||
const captures = temp.make("bundled-source-captures-");
|
||||
const entry = path.join(root, "index.ts");
|
||||
const writeEntry = (value: number) =>
|
||||
fs.writeFileSync(
|
||||
entry,
|
||||
`import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/plugin-entry";
|
||||
export const value: number = ${value};
|
||||
export const schema = emptyPluginConfigSchema().jsonSchema;`,
|
||||
);
|
||||
const load = () => {
|
||||
const instance = new PluginInstance("bundled-source");
|
||||
instances.push(instance);
|
||||
const value = withPluginSourceCaptureDirectory(captures, () => {
|
||||
bindPluginInstanceModuleLoader({
|
||||
instance,
|
||||
origin: "bundled",
|
||||
source: entry,
|
||||
rootDir: root,
|
||||
});
|
||||
return instance.loadModule(entry);
|
||||
});
|
||||
expect(instance.sourceDigest).toBeUndefined();
|
||||
expect(fs.readdirSync(captures)).toEqual([]);
|
||||
expect(value).toMatchObject({ value: 1, schema: { type: "object" } });
|
||||
return instance;
|
||||
};
|
||||
|
||||
writeEntry(1);
|
||||
expect(getSharedPluginCodeReloadWarning(load())).toBeUndefined();
|
||||
resetPluginCache();
|
||||
writeEntry(2);
|
||||
expect(getSharedPluginCodeReloadWarning(load())).toBe(
|
||||
"Bundled plugin code remains loaded. Restart the Gateway to load edited code.",
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["cjs", "mjs"])(
|
||||
"reuses compiled bundled %s code while giving recovery fresh callback authority",
|
||||
"reuses bundled %s code while giving recovery fresh callback authority",
|
||||
async (extension) => {
|
||||
const root = temp.make("bundled-recovery-identity-");
|
||||
const entry = path.join(root, `index.${extension}`);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const state = resolveGlobalSingleton(Symbol.for("openclaw.sharedPluginModuleIden
|
|||
}));
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
/** The loaded module owns its original artifact identity across registry replacements. */
|
||||
/** Canonical exports retain file identity across Jiti wrappers and registry replacements. */
|
||||
export function bindSharedPluginModuleLoader(params: {
|
||||
instance: PluginModuleLoaderOwner;
|
||||
rootDir: string;
|
||||
|
|
@ -57,11 +57,13 @@ function createSharedModuleBinding(
|
|||
if ((typeof value !== "object" || value === null) && typeof value !== "function") {
|
||||
return value;
|
||||
}
|
||||
let identities = state.sources.get(value);
|
||||
if (!identities) {
|
||||
identities = new Map();
|
||||
state.sources.set(value, identities);
|
||||
}
|
||||
const exported: unknown = Reflect.get(value, "default");
|
||||
const identity =
|
||||
exported && (typeof exported === "object" || typeof exported === "function")
|
||||
? exported
|
||||
: value;
|
||||
const identities = state.sources.get(identity) ?? new Map<string, string | undefined>();
|
||||
state.sources.set(identity, identities);
|
||||
if (!identities.has(root)) {
|
||||
identities.set(root, currentIdentity);
|
||||
} else {
|
||||
|
|
@ -70,8 +72,8 @@ function createSharedModuleBinding(
|
|||
state.warnings.set(
|
||||
target,
|
||||
!loadedIdentity || !currentIdentity
|
||||
? "Compiled bundled plugin code remains loaded and its files could not be verified. Restart the Gateway to load edited code."
|
||||
: "Compiled bundled plugin code remains loaded. Restart the Gateway to load edited code.",
|
||||
? "Bundled plugin code remains loaded and its files could not be verified. Restart the Gateway to load edited code."
|
||||
: "Bundled plugin code remains loaded. Restart the Gateway to load edited code.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@ function preparePolicy(root: string, version: string) {
|
|||
const canonicalize = () => canonicalizeProviderModelId("policy-fixture", "authored-id");
|
||||
|
||||
describe("provider policy generations", () => {
|
||||
it("reuses published policies and resolves replacement and retained generations through their owners", () => {
|
||||
it("reuses bundled policy libraries across registry selection and retirement", () => {
|
||||
const root = tempDirs.make("openclaw-policy-generation-");
|
||||
const firstCache = createPluginCache();
|
||||
const { registry: first, record: firstRecord } = withPluginCache(firstCache, () =>
|
||||
preparePolicy(root, "first"),
|
||||
preparePolicy(path.join(root, "first"), "first"),
|
||||
);
|
||||
stageActivePluginRegistry(first, "first", "default");
|
||||
const candidates = vi.spyOn(
|
||||
|
|
@ -74,7 +74,9 @@ describe("provider policy generations", () => {
|
|||
expect(candidates).toHaveBeenCalledTimes(1);
|
||||
|
||||
const nextCache = createPluginCache();
|
||||
const { registry: next } = withPluginCache(nextCache, () => preparePolicy(root, "replacement"));
|
||||
const { registry: next } = withPluginCache(nextCache, () =>
|
||||
preparePolicy(path.join(root, "replacement"), "replacement"),
|
||||
);
|
||||
stageActivePluginRegistry(next, "replacement", "default");
|
||||
const readNext = () =>
|
||||
withPluginCache(nextCache, () => withPluginRuntimeRegistryScope(next, canonicalize));
|
||||
|
|
@ -97,13 +99,7 @@ describe("provider policy generations", () => {
|
|||
expect(candidates).toHaveBeenCalledTimes(6);
|
||||
|
||||
getPluginInstance(firstRecord)!.quiesce();
|
||||
expect(() =>
|
||||
withPluginCache(firstCache, () =>
|
||||
withPluginRuntimeRegistryScope(first, () =>
|
||||
resolveDirectBundledProviderPolicySurface("policy-fixture"),
|
||||
),
|
||||
),
|
||||
).toThrow("Plugin policy-fixture was reloaded or disabled");
|
||||
expect(readFirst()).toBe("first");
|
||||
expect(candidates).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
|
||||
|
|
@ -116,7 +112,7 @@ describe("provider policy generations", () => {
|
|||
withPluginRuntimeRegistryScope(registry, () => {
|
||||
expect(resolveDirectBundledProviderPolicySurface("policy-fixture")).toBeNull();
|
||||
const { registry: prepared } = preparePolicy(
|
||||
path.join(root, "policy-fixture"),
|
||||
path.join(root, "selected", "policy-fixture"),
|
||||
"new-owner",
|
||||
);
|
||||
registry.plugins.push(...prepared.plugins);
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ function prepare(
|
|||
}
|
||||
|
||||
describe("managed plugin public surfaces", () => {
|
||||
it("keeps a prepared source overlay's registered web provider under a merged environment", async () => {
|
||||
it("keeps a prepared source overlay's setup web provider under a merged environment", async () => {
|
||||
const parent = temp.make("openclaw-web-provider-overlay-");
|
||||
const pluginId = "overlay-web-provider";
|
||||
const bundledDir = path.join(parent, "stock");
|
||||
|
|
@ -146,7 +146,6 @@ describe("managed plugin public surfaces", () => {
|
|||
vi.stubEnv("OPENCLAW_DISABLE_BUNDLED_PLUGINS", "0");
|
||||
// Secret refresh copies env while retaining the selected manifest and live source generation.
|
||||
const env = { ...process.env };
|
||||
fs.rmSync(overlayArtifact);
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
env,
|
||||
mode: "setup",
|
||||
|
|
@ -166,7 +165,7 @@ describe("managed plugin public surfaces", () => {
|
|||
const provider = expectDefined(providers[0], "retained overlay web provider");
|
||||
expect(provider.getCredentialValue()).toBe("registered-overlay");
|
||||
await active.instance.dispose();
|
||||
expect(() => provider.getCredentialValue()).toThrow(/reloaded|disabled|retiring/);
|
||||
expect(provider.getCredentialValue()).toBe("registered-overlay");
|
||||
});
|
||||
|
||||
it.each(["id", "folder", "channel"] as const)(
|
||||
|
|
@ -216,136 +215,127 @@ describe("managed plugin public surfaces", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it.each(["global", "bundled"] as const)(
|
||||
"resolves native filesystem dist APIs without losing their managed owner (%s)",
|
||||
(origin) => {
|
||||
const hooks = Reflect.get(Module, "registerHooks");
|
||||
Reflect.set(Module, "registerHooks", undefined);
|
||||
try {
|
||||
const root = fs.realpathSync(temp.make("openclaw-native-public-dist-"));
|
||||
writeSource(root, "library");
|
||||
fs.rmSync(path.join(root, "api.ts"));
|
||||
fs.rmSync(path.join(root, "runtime-api.ts"));
|
||||
fs.mkdirSync(path.join(root, "dist"));
|
||||
fs.writeFileSync(path.join(root, "dist/api.js"), 'export { read } from "../state.js";');
|
||||
const active = prepare(root, "native-dist-fixture", origin);
|
||||
active.entry.default.register("registered");
|
||||
active.publish();
|
||||
const api = loadPluginPublicArtifactModuleSync<{ read(): string }>({
|
||||
pluginRoot: root,
|
||||
artifactBasename: "api.js",
|
||||
origin,
|
||||
});
|
||||
expect(api.read()).toBe("registered");
|
||||
expect(getPluginValueInstance(api)).toBe(active.instance);
|
||||
} finally {
|
||||
Reflect.set(Module, "registerHooks", hooks);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["global", "bundled"] as const)(
|
||||
"shares registration state with captured APIs, refreshes a retained facade, and fences old exports (%s)",
|
||||
async (origin) => {
|
||||
const root = fs.realpathSync(temp.make("openclaw-public-generation-"));
|
||||
writeSource(root, "source-one");
|
||||
const first = prepare(root, "surface-fixture", origin);
|
||||
first.entry.default.register("registered-one");
|
||||
first.publish();
|
||||
const loadApi = () =>
|
||||
loadPluginPublicArtifactModuleSync<PublicApi>({
|
||||
pluginRoot: root,
|
||||
artifactBasename: "api.js",
|
||||
});
|
||||
const loadFacade = () =>
|
||||
loadBundledPluginPublicSurfaceModuleSyncCore<PublicApi>({
|
||||
dirName: first.record.id,
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
const lazy = createLazyFacadeObjectValue(() => loadFacade().view);
|
||||
const frozen = createLazyFacadeObjectValue(() => loadFacade().view.frozen);
|
||||
|
||||
// Neither public entry has executed yet. The captured graph survives source deletion.
|
||||
it("resolves installed native filesystem dist APIs without losing their managed owner", () => {
|
||||
const hooks = Reflect.get(Module, "registerHooks");
|
||||
Reflect.set(Module, "registerHooks", undefined);
|
||||
try {
|
||||
const root = fs.realpathSync(temp.make("openclaw-native-public-dist-"));
|
||||
writeSource(root, "library");
|
||||
fs.rmSync(path.join(root, "api.ts"));
|
||||
fs.rmSync(path.join(root, "runtime-api.ts"));
|
||||
fs.writeFileSync(
|
||||
path.join(root, "state.ts"),
|
||||
'export const read = () => "uncommitted edit";',
|
||||
);
|
||||
const originalApi = loadApi();
|
||||
const originalRead = originalApi.read;
|
||||
expect(originalRead()).toBe("registered-one");
|
||||
expect(loadFacade().read()).toBe("registered-one");
|
||||
expect(lazy.read()).toBe("registered-one");
|
||||
const retainedLazyRead = lazy.read;
|
||||
for (const [property, descriptor] of [
|
||||
["read", { configurable: false, writable: false }],
|
||||
["added", { value: "fixed" }],
|
||||
[Symbol("fixed"), { get: () => "fixed", configurable: false }],
|
||||
] as const) {
|
||||
const before = Object.getOwnPropertyDescriptor(originalApi.view, property);
|
||||
expect(Reflect.defineProperty(lazy, property, descriptor)).toBe(false);
|
||||
expect(Object.getOwnPropertyDescriptor(originalApi.view, property)).toEqual(before);
|
||||
}
|
||||
expect(Reflect.set(lazy, "note", "first")).toBe(true);
|
||||
expect(Reflect.defineProperty(lazy, "note", { value: "updated" })).toBe(true);
|
||||
expect(Reflect.get(originalApi.view, "note")).toBe("updated");
|
||||
const configurable = Symbol("configurable");
|
||||
expect(
|
||||
Reflect.defineProperty(lazy, configurable, { value: "allowed", configurable: true }),
|
||||
).toBe(true);
|
||||
expect(Reflect.get(originalApi.view, configurable)).toBe("allowed");
|
||||
expect(Object.getOwnPropertyDescriptor(frozen, "length")).toMatchObject({
|
||||
value: 1,
|
||||
configurable: true,
|
||||
fs.mkdirSync(path.join(root, "dist"));
|
||||
fs.writeFileSync(path.join(root, "dist/api.js"), 'export { read } from "../state.js";');
|
||||
const active = prepare(root, "native-dist-fixture");
|
||||
active.entry.default.register("registered");
|
||||
active.publish();
|
||||
const api = loadPluginPublicArtifactModuleSync<{ read(): string }>({
|
||||
pluginRoot: root,
|
||||
artifactBasename: "api.js",
|
||||
origin: "global",
|
||||
});
|
||||
expect(Object.keys(frozen)).toEqual(["0"]);
|
||||
for (const operation of ["preventExtensions", "seal", "freeze"] as const) {
|
||||
const applyIntegrity: (value: object) => object = Object[operation];
|
||||
expect(() => applyIntegrity(lazy)).toThrow(TypeError);
|
||||
expect(Object.isExtensible(lazy)).toBe(true);
|
||||
expect(lazy.read()).toBe("registered-one");
|
||||
}
|
||||
const probes = [
|
||||
vi.spyOn(fs, "existsSync"),
|
||||
vi.spyOn(fs, "realpathSync"),
|
||||
vi.spyOn(fs, "statSync"),
|
||||
vi.spyOn(fs, "openSync"),
|
||||
];
|
||||
expect(loadApi()).toBe(originalApi);
|
||||
expect(api.read()).toBe("registered");
|
||||
expect(getPluginValueInstance(api)).toBe(active.instance);
|
||||
} finally {
|
||||
Reflect.set(Module, "registerHooks", hooks);
|
||||
}
|
||||
});
|
||||
|
||||
it("shares installed registration state with captured APIs, refreshes a retained facade, and fences old exports", async () => {
|
||||
const root = fs.realpathSync(temp.make("openclaw-public-generation-"));
|
||||
writeSource(root, "source-one");
|
||||
const first = prepare(root);
|
||||
first.entry.default.register("registered-one");
|
||||
first.publish();
|
||||
const loadApi = () =>
|
||||
loadPluginPublicArtifactModuleSync<PublicApi>({
|
||||
pluginRoot: root,
|
||||
artifactBasename: "api.js",
|
||||
});
|
||||
const loadFacade = () =>
|
||||
loadBundledPluginPublicSurfaceModuleSyncCore<PublicApi>({
|
||||
dirName: first.record.id,
|
||||
artifactBasename: "runtime-api.js",
|
||||
});
|
||||
const lazy = createLazyFacadeObjectValue(() => loadFacade().view);
|
||||
const frozen = createLazyFacadeObjectValue(() => loadFacade().view.frozen);
|
||||
|
||||
// Neither public entry has executed yet. The captured graph survives source deletion.
|
||||
fs.rmSync(path.join(root, "api.ts"));
|
||||
fs.rmSync(path.join(root, "runtime-api.ts"));
|
||||
fs.writeFileSync(path.join(root, "state.ts"), 'export const read = () => "uncommitted edit";');
|
||||
const originalApi = loadApi();
|
||||
const originalRead = originalApi.read;
|
||||
expect(originalRead()).toBe("registered-one");
|
||||
expect(loadFacade().read()).toBe("registered-one");
|
||||
expect(lazy.read()).toBe("registered-one");
|
||||
const retainedLazyRead = lazy.read;
|
||||
for (const [property, descriptor] of [
|
||||
["read", { configurable: false, writable: false }],
|
||||
["added", { value: "fixed" }],
|
||||
[Symbol("fixed"), { get: () => "fixed", configurable: false }],
|
||||
] as const) {
|
||||
const before = Object.getOwnPropertyDescriptor(originalApi.view, property);
|
||||
expect(Reflect.defineProperty(lazy, property, descriptor)).toBe(false);
|
||||
expect(Object.getOwnPropertyDescriptor(originalApi.view, property)).toEqual(before);
|
||||
}
|
||||
expect(Reflect.set(lazy, "note", "first")).toBe(true);
|
||||
expect(Reflect.defineProperty(lazy, "note", { value: "updated" })).toBe(true);
|
||||
expect(Reflect.get(originalApi.view, "note")).toBe("updated");
|
||||
const configurable = Symbol("configurable");
|
||||
expect(
|
||||
Reflect.defineProperty(lazy, configurable, { value: "allowed", configurable: true }),
|
||||
).toBe(true);
|
||||
expect(Reflect.get(originalApi.view, configurable)).toBe("allowed");
|
||||
expect(Object.getOwnPropertyDescriptor(frozen, "length")).toMatchObject({
|
||||
value: 1,
|
||||
configurable: true,
|
||||
});
|
||||
expect(Object.keys(frozen)).toEqual(["0"]);
|
||||
for (const operation of ["preventExtensions", "seal", "freeze"] as const) {
|
||||
const applyIntegrity: (value: object) => object = Object[operation];
|
||||
expect(() => applyIntegrity(lazy)).toThrow(TypeError);
|
||||
expect(Object.isExtensible(lazy)).toBe(true);
|
||||
expect(lazy.read()).toBe("registered-one");
|
||||
for (const probe of probes) {
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
probe.mockRestore();
|
||||
}
|
||||
}
|
||||
const probes = [
|
||||
vi.spyOn(fs, "existsSync"),
|
||||
vi.spyOn(fs, "realpathSync"),
|
||||
vi.spyOn(fs, "statSync"),
|
||||
vi.spyOn(fs, "openSync"),
|
||||
];
|
||||
expect(loadApi()).toBe(originalApi);
|
||||
expect(lazy.read()).toBe("registered-one");
|
||||
for (const probe of probes) {
|
||||
expect(probe).not.toHaveBeenCalled();
|
||||
probe.mockRestore();
|
||||
}
|
||||
|
||||
await first.instance.dispose();
|
||||
writeSource(root, "source-two", "ts", 2);
|
||||
const second = prepare(root, "surface-fixture", origin);
|
||||
second.entry.default.register("registered-two");
|
||||
second.publish();
|
||||
expect(loadApi().read()).toBe("registered-two");
|
||||
expect(lazy.read()).toBe("registered-two");
|
||||
expect(frozen.length).toBe(2);
|
||||
expect(Object.keys(frozen)).toEqual(["0", "1"]);
|
||||
expect(frozen[1]?.()).toBe("registered-two");
|
||||
expect(originalRead).toThrow(/reloaded|disabled|retiring/);
|
||||
expect(retainedLazyRead).toThrow(/reloaded|disabled|retiring/);
|
||||
expect(() =>
|
||||
withPluginRuntimeGatewayRequestScope(
|
||||
{ pluginRegistry: first.registry, isWebchatConnect: () => false },
|
||||
loadApi,
|
||||
),
|
||||
).toThrow(MissingPublicSurfaceError);
|
||||
await first.instance.dispose();
|
||||
writeSource(root, "source-two", "ts", 2);
|
||||
const second = prepare(root);
|
||||
second.entry.default.register("registered-two");
|
||||
second.publish();
|
||||
expect(loadApi().read()).toBe("registered-two");
|
||||
expect(lazy.read()).toBe("registered-two");
|
||||
expect(frozen.length).toBe(2);
|
||||
expect(Object.keys(frozen)).toEqual(["0", "1"]);
|
||||
expect(frozen[1]?.()).toBe("registered-two");
|
||||
expect(originalRead).toThrow(/reloaded|disabled|retiring/);
|
||||
expect(retainedLazyRead).toThrow(/reloaded|disabled|retiring/);
|
||||
expect(() =>
|
||||
withPluginRuntimeGatewayRequestScope(
|
||||
{ pluginRegistry: first.registry, isWebchatConnect: () => false },
|
||||
loadApi,
|
||||
),
|
||||
).toThrow(MissingPublicSurfaceError);
|
||||
|
||||
const retained = loadApi();
|
||||
const nextRegistry = createEmptyPluginRegistry();
|
||||
nextRegistry.plugins.push(second.record);
|
||||
stageActivePluginRegistry(nextRegistry, "unrelated-change", "default");
|
||||
expect(loadApi()).toBe(retained);
|
||||
expect(lazy.read()).toBe("registered-two");
|
||||
},
|
||||
);
|
||||
const retained = loadApi();
|
||||
const nextRegistry = createEmptyPluginRegistry();
|
||||
nextRegistry.plugins.push(second.record);
|
||||
stageActivePluginRegistry(nextRegistry, "unrelated-change", "default");
|
||||
expect(loadApi()).toBe(retained);
|
||||
expect(lazy.read()).toBe("registered-two");
|
||||
});
|
||||
|
||||
it.each(["relative", "tsconfig"] as const)(
|
||||
"loads captured dependencies and %s source mappings after originals are removed",
|
||||
|
|
@ -571,15 +561,8 @@ describe("managed plugin public surfaces", () => {
|
|||
stageActivePluginRegistry(disabled, "disabled", "gateway-bindable");
|
||||
await active.instance.dispose();
|
||||
const inspection = loadBundledPluginPublicSurfaceModuleSyncCore<PublicApi>(request);
|
||||
if (extension === "js") {
|
||||
expect(inspection).toBe(library);
|
||||
expect(library.read()).toBe("registered");
|
||||
} else {
|
||||
// Vitest deep-compares unequal references; retired export getters must stay fenced.
|
||||
expect(Object.is(inspection, library)).toBe(false);
|
||||
expect(inspection.read()).toBe("library");
|
||||
expect(() => library.read()).toThrow(/reloaded|disabled|retiring/);
|
||||
}
|
||||
expect(inspection).toBe(library);
|
||||
expect(library.read()).toBe("registered");
|
||||
expect(retainedRead).toThrow(/reloaded|disabled|retiring/);
|
||||
expect(() => runtime.read()).toThrow(/reloaded or disabled/);
|
||||
await expect(async () => await loadActivated(request)).rejects.toThrow(/access blocked/);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue