mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:10:52 +00:00
fix: propagate update timeout to plugin installs
This commit is contained in:
@@ -524,11 +524,11 @@ describe("update-cli", () => {
|
||||
it("respawns into the updated package root before running post-update tasks", async () => {
|
||||
const { entrypoints } = setupUpdatedRootRefresh();
|
||||
|
||||
await updateCommand({ yes: true });
|
||||
await updateCommand({ yes: true, timeout: "1800" });
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/node/),
|
||||
[entrypoints[0], "update", "--yes"],
|
||||
[entrypoints[0], "update", "--yes", "--timeout", "1800"],
|
||||
expect.objectContaining({
|
||||
stdio: "inherit",
|
||||
env: expect.objectContaining({
|
||||
@@ -625,6 +625,22 @@ describe("update-cli", () => {
|
||||
expect(spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes the update timeout budget into post-core plugin updates", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_UPDATE_POST_CORE: "1",
|
||||
OPENCLAW_UPDATE_POST_CORE_CHANNEL: "stable",
|
||||
},
|
||||
async () => {
|
||||
await updateCommand({ restart: false, timeout: "1800" });
|
||||
},
|
||||
);
|
||||
|
||||
expect(updateNpmInstalledPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ timeoutMs: 1_800_000 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses a fail-closed integrity policy for post-core plugin updates", async () => {
|
||||
await withEnvAsync(
|
||||
{
|
||||
|
||||
@@ -89,6 +89,7 @@ import { suppressDeprecations } from "./suppress-deprecations.js";
|
||||
|
||||
const CLI_NAME = resolveCliName();
|
||||
const SERVICE_REFRESH_TIMEOUT_MS = 60_000;
|
||||
const DEFAULT_UPDATE_STEP_TIMEOUT_MS = 20 * 60_000;
|
||||
const POST_CORE_UPDATE_ENV = "OPENCLAW_UPDATE_POST_CORE";
|
||||
const POST_CORE_UPDATE_CHANNEL_ENV = "OPENCLAW_UPDATE_POST_CORE_CHANNEL";
|
||||
const POST_CORE_UPDATE_RESULT_PATH_ENV = "OPENCLAW_UPDATE_POST_CORE_RESULT_PATH";
|
||||
@@ -455,7 +456,7 @@ async function runGitUpdate(params: {
|
||||
devTargetRef?: string;
|
||||
}): Promise<UpdateRunResult> {
|
||||
const updateRoot = params.switchToGit ? resolveGitInstallDir() : params.root;
|
||||
const effectiveTimeout = params.timeoutMs ?? 20 * 60_000;
|
||||
const effectiveTimeout = params.timeoutMs ?? DEFAULT_UPDATE_STEP_TIMEOUT_MS;
|
||||
const installEnv = await createGlobalInstallEnv();
|
||||
|
||||
const cloneStep = params.switchToGit
|
||||
@@ -537,6 +538,7 @@ async function updatePluginsAfterCoreUpdate(params: {
|
||||
channel: "stable" | "beta" | "dev";
|
||||
configSnapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>;
|
||||
opts: UpdateCommandOptions;
|
||||
timeoutMs: number;
|
||||
}): Promise<PostCorePluginUpdateResult> {
|
||||
if (!params.configSnapshot.valid) {
|
||||
if (!params.opts.json) {
|
||||
@@ -589,6 +591,7 @@ async function updatePluginsAfterCoreUpdate(params: {
|
||||
|
||||
const npmResult = await updateNpmInstalledPlugins({
|
||||
config: pluginConfig,
|
||||
timeoutMs: params.timeoutMs,
|
||||
skipIds: new Set(syncResult.summary.switchedToNpm),
|
||||
logger: pluginLogger,
|
||||
onIntegrityDrift: async (drift) => {
|
||||
@@ -896,12 +899,14 @@ async function runPostCorePluginUpdate(params: {
|
||||
channel: "stable" | "beta" | "dev";
|
||||
configSnapshot: Awaited<ReturnType<typeof readConfigFileSnapshot>>;
|
||||
opts: UpdateCommandOptions;
|
||||
timeoutMs: number;
|
||||
}): Promise<PostCorePluginUpdateResult> {
|
||||
return await updatePluginsAfterCoreUpdate({
|
||||
root: params.root,
|
||||
channel: params.channel,
|
||||
configSnapshot: params.configSnapshot,
|
||||
opts: params.opts,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -954,6 +959,9 @@ async function continuePostCoreUpdateInFreshProcess(params: {
|
||||
if (params.opts.yes) {
|
||||
argv.push("--yes");
|
||||
}
|
||||
if (params.opts.timeout) {
|
||||
argv.push("--timeout", params.opts.timeout);
|
||||
}
|
||||
const resultDir =
|
||||
params.opts.json === true
|
||||
? await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-post-core-"))
|
||||
@@ -1018,6 +1026,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
if (timeoutMs === null) {
|
||||
return;
|
||||
}
|
||||
const updateStepTimeoutMs = timeoutMs ?? DEFAULT_UPDATE_STEP_TIMEOUT_MS;
|
||||
|
||||
const root = await resolveUpdateRoot();
|
||||
if (postCoreUpdateResume) {
|
||||
@@ -1036,6 +1045,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
channel: postCoreUpdateChannel,
|
||||
configSnapshot: await readConfigFileSnapshot(),
|
||||
opts,
|
||||
timeoutMs: updateStepTimeoutMs,
|
||||
});
|
||||
if (opts.json) {
|
||||
await writePostCorePluginUpdateResultFile(
|
||||
@@ -1146,7 +1156,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
mode = await resolveGlobalManager({
|
||||
root,
|
||||
installKind,
|
||||
timeoutMs: timeoutMs ?? 20 * 60_000,
|
||||
timeoutMs: updateStepTimeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1271,7 +1281,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
root,
|
||||
installKind,
|
||||
tag,
|
||||
timeoutMs: timeoutMs ?? 20 * 60_000,
|
||||
timeoutMs: updateStepTimeoutMs,
|
||||
startedAt,
|
||||
progress,
|
||||
jsonMode: Boolean(opts.json),
|
||||
@@ -1414,6 +1424,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise<void> {
|
||||
channel,
|
||||
configSnapshot: postUpdateConfigSnapshot,
|
||||
opts,
|
||||
timeoutMs: updateStepTimeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -594,6 +594,7 @@ async function resolveCompatiblePackageVersion(params: {
|
||||
requestedVersion?: string;
|
||||
baseUrl?: string;
|
||||
token?: string;
|
||||
timeoutMs?: number;
|
||||
}): Promise<
|
||||
| {
|
||||
ok: true;
|
||||
@@ -617,6 +618,7 @@ async function resolveCompatiblePackageVersion(params: {
|
||||
version: requestedVersion,
|
||||
baseUrl: params.baseUrl,
|
||||
token: params.token,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
} catch (error) {
|
||||
return mapClawHubRequestError(error, {
|
||||
@@ -747,6 +749,7 @@ export async function installPluginFromClawHub(
|
||||
logger?: PluginInstallLogger;
|
||||
mode?: "install" | "update";
|
||||
extensionsDir?: string;
|
||||
timeoutMs?: number;
|
||||
dryRun?: boolean;
|
||||
expectedPluginId?: string;
|
||||
},
|
||||
@@ -775,6 +778,7 @@ export async function installPluginFromClawHub(
|
||||
name: parsed.name,
|
||||
baseUrl: params.baseUrl,
|
||||
token: params.token,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
} catch (error) {
|
||||
return mapClawHubRequestError(error, {
|
||||
@@ -787,6 +791,7 @@ export async function installPluginFromClawHub(
|
||||
requestedVersion: parsed.version,
|
||||
baseUrl: params.baseUrl,
|
||||
token: params.token,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
if (!versionState.ok) {
|
||||
return versionState;
|
||||
@@ -821,6 +826,7 @@ export async function installPluginFromClawHub(
|
||||
version: versionState.version,
|
||||
baseUrl: params.baseUrl,
|
||||
token: params.token,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
} catch (error) {
|
||||
return buildClawHubInstallFailure(formatErrorMessage(error));
|
||||
@@ -864,6 +870,7 @@ export async function installPluginFromClawHub(
|
||||
logger: params.logger,
|
||||
mode: params.mode,
|
||||
extensionsDir: params.extensionsDir,
|
||||
timeoutMs: params.timeoutMs,
|
||||
dryRun: params.dryRun,
|
||||
expectedPluginId: params.expectedPluginId,
|
||||
});
|
||||
|
||||
@@ -1156,6 +1156,7 @@ export async function installPluginFromMarketplace(
|
||||
logger: params.logger,
|
||||
mode: params.mode,
|
||||
extensionsDir: params.extensionsDir,
|
||||
timeoutMs: params.timeoutMs,
|
||||
dryRun: params.dryRun,
|
||||
expectedPluginId: params.expectedPluginId,
|
||||
});
|
||||
|
||||
@@ -214,12 +214,14 @@ function expectNpmUpdateCall(params: {
|
||||
spec: string;
|
||||
expectedIntegrity?: string;
|
||||
expectedPluginId?: string;
|
||||
timeoutMs?: number;
|
||||
}) {
|
||||
expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: params.spec,
|
||||
expectedIntegrity: params.expectedIntegrity,
|
||||
...(params.expectedPluginId ? { expectedPluginId: params.expectedPluginId } : {}),
|
||||
...(params.timeoutMs ? { timeoutMs: params.timeoutMs } : {}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -355,6 +357,48 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("passes timeout budget to npm plugin metadata checks and installs", async () => {
|
||||
const installPath = createInstalledPackageDir({
|
||||
name: "@martian-engineering/lossless-claw",
|
||||
version: "0.9.0",
|
||||
});
|
||||
mockNpmViewMetadata({
|
||||
name: "@martian-engineering/lossless-claw",
|
||||
version: "0.10.0",
|
||||
integrity: "sha512-next",
|
||||
});
|
||||
installPluginFromNpmSpecMock.mockResolvedValue(
|
||||
createSuccessfulNpmUpdateResult({
|
||||
pluginId: "lossless-claw",
|
||||
targetDir: installPath,
|
||||
version: "0.10.0",
|
||||
}),
|
||||
);
|
||||
|
||||
await updateNpmInstalledPlugins({
|
||||
config: createNpmInstallConfig({
|
||||
pluginId: "lossless-claw",
|
||||
spec: "@martian-engineering/lossless-claw",
|
||||
installPath,
|
||||
resolvedName: "@martian-engineering/lossless-claw",
|
||||
resolvedSpec: "@martian-engineering/lossless-claw@0.9.0",
|
||||
resolvedVersion: "0.9.0",
|
||||
}),
|
||||
pluginIds: ["lossless-claw"],
|
||||
timeoutMs: 1_800_000,
|
||||
});
|
||||
|
||||
const npmViewCall = runCommandWithTimeoutMock.mock.calls.find(
|
||||
([argv]) => Array.isArray(argv) && argv[0] === "npm" && argv[1] === "view",
|
||||
);
|
||||
expect(npmViewCall?.[1]).toEqual(expect.objectContaining({ timeoutMs: 1_800_000 }));
|
||||
expectNpmUpdateCall({
|
||||
spec: "@martian-engineering/lossless-claw",
|
||||
expectedPluginId: "lossless-claw",
|
||||
timeoutMs: 1_800_000,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips npm reinstall and config rewrite when the installed artifact is unchanged", async () => {
|
||||
const installPath = createInstalledPackageDir({
|
||||
name: "@martian-engineering/lossless-claw",
|
||||
@@ -798,6 +842,7 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
clawhubChannel: "official",
|
||||
}),
|
||||
pluginIds: ["demo"],
|
||||
timeoutMs: 1_800_000,
|
||||
});
|
||||
|
||||
expect(installPluginFromClawHubMock).toHaveBeenCalledWith(
|
||||
@@ -806,6 +851,7 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
baseUrl: "https://clawhub.ai",
|
||||
expectedPluginId: "demo",
|
||||
mode: "update",
|
||||
timeoutMs: 1_800_000,
|
||||
}),
|
||||
);
|
||||
expect(result.config.plugins?.installs?.demo).toMatchObject({
|
||||
@@ -930,6 +976,7 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
marketplacePlugin: "claude-bundle",
|
||||
}),
|
||||
pluginIds: ["claude-bundle"],
|
||||
timeoutMs: 1_800_000,
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
@@ -939,6 +986,7 @@ describe("updateNpmInstalledPlugins", () => {
|
||||
plugin: "claude-bundle",
|
||||
expectedPluginId: "claude-bundle",
|
||||
dryRun: true,
|
||||
timeoutMs: 1_800_000,
|
||||
}),
|
||||
);
|
||||
expect(result.outcomes).toEqual([
|
||||
|
||||
@@ -469,6 +469,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
logger?: PluginUpdateLogger;
|
||||
pluginIds?: string[];
|
||||
skipIds?: Set<string>;
|
||||
timeoutMs?: number;
|
||||
dryRun?: boolean;
|
||||
dangerouslyForceUnsafeInstall?: boolean;
|
||||
specOverrides?: Record<string, string>;
|
||||
@@ -567,7 +568,10 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
});
|
||||
|
||||
if (!params.dryRun && record.source === "npm" && currentVersion) {
|
||||
const metadataResult = await resolveNpmSpecMetadata({ spec: effectiveSpec! });
|
||||
const metadataResult = await resolveNpmSpecMetadata({
|
||||
spec: effectiveSpec!,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
if (metadataResult.ok) {
|
||||
if (
|
||||
shouldSkipUnchangedNpmInstall({
|
||||
@@ -604,6 +608,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
spec: effectiveSpec!,
|
||||
mode: "update",
|
||||
extensionsDir,
|
||||
timeoutMs: params.timeoutMs,
|
||||
dryRun: true,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
@@ -622,6 +627,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
baseUrl: record.clawhubUrl,
|
||||
mode: "update",
|
||||
extensionsDir,
|
||||
timeoutMs: params.timeoutMs,
|
||||
dryRun: true,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
@@ -632,6 +638,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
plugin: record.marketplacePlugin!,
|
||||
mode: "update",
|
||||
extensionsDir,
|
||||
timeoutMs: params.timeoutMs,
|
||||
dryRun: true,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
@@ -708,6 +715,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
spec: effectiveSpec!,
|
||||
mode: "update",
|
||||
extensionsDir,
|
||||
timeoutMs: params.timeoutMs,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
expectedIntegrity,
|
||||
@@ -725,6 +733,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
baseUrl: record.clawhubUrl,
|
||||
mode: "update",
|
||||
extensionsDir,
|
||||
timeoutMs: params.timeoutMs,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
logger,
|
||||
@@ -734,6 +743,7 @@ export async function updateNpmInstalledPlugins(params: {
|
||||
plugin: record.marketplacePlugin!,
|
||||
mode: "update",
|
||||
extensionsDir,
|
||||
timeoutMs: params.timeoutMs,
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
expectedPluginId: pluginId,
|
||||
logger,
|
||||
|
||||
Reference in New Issue
Block a user