mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-06 01:01:35 +00:00
fix: release ClawHub lifecycle state on CLI exit (#114090)
This commit is contained in:
committed by
GitHub
parent
9eae43bd37
commit
53e3f296b1
@@ -5,6 +5,11 @@ import { resolvePluginUninstallId } from "./plugins-uninstall-selection.js";
|
||||
|
||||
describe("resolvePluginUninstallId", () => {
|
||||
it("accepts the recorded ClawHub spec as an uninstall target", () => {
|
||||
const plugin = {
|
||||
id: "linkmind-context",
|
||||
name: "linkmind-context",
|
||||
channelIds: ["linkmind-channel"],
|
||||
};
|
||||
const result = resolvePluginUninstallId({
|
||||
rawId: "clawhub:linkmind-context",
|
||||
config: {
|
||||
@@ -21,13 +26,19 @@ describe("resolvePluginUninstallId", () => {
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
plugins: [{ id: "linkmind-context", name: "linkmind-context" }],
|
||||
plugins: [plugin],
|
||||
});
|
||||
|
||||
expect(result.pluginId).toBe("linkmind-context");
|
||||
expect(result.plugin).toBe(plugin);
|
||||
});
|
||||
|
||||
it("accepts a versionless ClawHub spec when the install was pinned", () => {
|
||||
const plugin = {
|
||||
id: "linkmind-context",
|
||||
name: "linkmind-context",
|
||||
channelIds: ["linkmind-channel"],
|
||||
};
|
||||
const result = resolvePluginUninstallId({
|
||||
rawId: "clawhub:linkmind-context",
|
||||
config: {
|
||||
@@ -43,9 +54,10 @@ describe("resolvePluginUninstallId", () => {
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
plugins: [{ id: "linkmind-context", name: "linkmind-context" }],
|
||||
plugins: [plugin],
|
||||
});
|
||||
|
||||
expect(result.pluginId).toBe("linkmind-context");
|
||||
expect(result.plugin).toBe(plugin);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,10 @@ export function resolvePluginUninstallId<
|
||||
plugins: TPlugin[];
|
||||
}): { pluginId: string; plugin?: TPlugin } {
|
||||
const rawId = params.rawId.trim();
|
||||
const resolveInstalledPlugin = (pluginId: string) => {
|
||||
const plugin = params.plugins.find((entry) => entry.id === pluginId);
|
||||
return plugin ? { pluginId, plugin } : { pluginId };
|
||||
};
|
||||
const plugin = params.plugins.find((entry) => entry.id === rawId || entry.name === rawId);
|
||||
if (plugin) {
|
||||
return { pluginId: plugin.id, plugin };
|
||||
@@ -24,7 +28,7 @@ export function resolvePluginUninstallId<
|
||||
install.resolvedName === rawId ||
|
||||
install.marketplacePlugin === rawId
|
||||
) {
|
||||
return { pluginId };
|
||||
return resolveInstalledPlugin(pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +40,7 @@ export function resolvePluginUninstallId<
|
||||
parseClawHubPluginSpec(install.spec ?? "")?.name ??
|
||||
parseClawHubPluginSpec(install.resolvedSpec ?? "")?.name;
|
||||
if (installedClawHubName === requestedClawHub.name) {
|
||||
return { pluginId };
|
||||
return resolveInstalledPlugin(pluginId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
} from "../claws/provenance.js";
|
||||
import type { ClawAddPlan } from "../claws/types.js";
|
||||
import { markClawPackageIndependentlyOwned } from "./claw-package-adoption.js";
|
||||
import { acquireClawPackageLifecycleLease } from "./claw-package-lifecycle-lease.js";
|
||||
import {
|
||||
acquireClawPackageLifecycleLease,
|
||||
withClawPackageLifecycleLease,
|
||||
} from "./claw-package-lifecycle-lease.js";
|
||||
import { closeOpenClawStateDatabaseForTest } from "./openclaw-state-db.js";
|
||||
|
||||
afterEach(() => closeOpenClawStateDatabaseForTest());
|
||||
@@ -237,6 +240,34 @@ describe("Claw package independent adoption", () => {
|
||||
directLease?.release();
|
||||
});
|
||||
|
||||
it("releases a package lease when process exit bypasses async cleanup", async () => {
|
||||
const env = { OPENCLAW_STATE_DIR: tempDirs.make("claw-exit-lease-") };
|
||||
const artifact = { kind: "plugin", source: "clawhub", ref: "@acme/audit" } as const;
|
||||
const existingExitListeners = new Set(process.listeners("exit"));
|
||||
|
||||
await expect(
|
||||
withClawPackageLifecycleLease(
|
||||
artifact,
|
||||
async () => {
|
||||
const exitCleanup = process
|
||||
.listeners("exit")
|
||||
.find((listener) => !existingExitListeners.has(listener));
|
||||
expect(exitCleanup).toBeTypeOf("function");
|
||||
exitCleanup?.(1);
|
||||
throw new Error("simulated process exit");
|
||||
},
|
||||
{ env, required: true },
|
||||
),
|
||||
).rejects.toThrow("simulated process exit");
|
||||
|
||||
expect(
|
||||
process.listeners("exit").filter((listener) => !existingExitListeners.has(listener)),
|
||||
).toEqual([]);
|
||||
const nextLease = acquireClawPackageLifecycleLease(artifact, { env, required: true });
|
||||
expect(nextLease).not.toBeNull();
|
||||
nextLease?.release();
|
||||
});
|
||||
|
||||
it("fails open only for optional direct leases when lifecycle state is unavailable", () => {
|
||||
const invalidDatabasePath = tempDirs.make("claw-invalid-db-path-");
|
||||
const artifact = { kind: "plugin", source: "clawhub", ref: "@acme/audit" } as const;
|
||||
|
||||
@@ -205,11 +205,22 @@ export async function withClawPackageLifecycleLease<T>(
|
||||
return await operation();
|
||||
}
|
||||
const maintained = maintainClawPackageLifecycleLease(lease);
|
||||
// CLI failures call process.exit(), which skips async finally blocks. Release
|
||||
// synchronously on exit so the next package command is not blocked until TTL.
|
||||
const releaseOnExit = () => {
|
||||
try {
|
||||
maintained.release();
|
||||
} catch {
|
||||
// Expiry recovers a lease whose exit cleanup loses a database race.
|
||||
}
|
||||
};
|
||||
process.once("exit", releaseOnExit);
|
||||
try {
|
||||
const result = await operation();
|
||||
maintained.assertCurrent();
|
||||
return result;
|
||||
} finally {
|
||||
process.removeListener("exit", releaseOnExit);
|
||||
try {
|
||||
maintained.release();
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user