mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ui): retire stale install policy reviews
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
type ApplicationContextProvider,
|
||||
} from "../../test-helpers/application-context.ts";
|
||||
import type { PluginsRouteData } from "./plugins-page.ts";
|
||||
import type { PluginRowMessage } from "./view.ts";
|
||||
import "./plugins-page.ts";
|
||||
|
||||
type RequestHandler = (method: string, params: unknown) => Promise<unknown>;
|
||||
@@ -34,6 +35,7 @@ type TestPluginsPage = HTMLElement & {
|
||||
result: PluginListResult | null;
|
||||
loading: boolean;
|
||||
busy: Record<string, boolean>;
|
||||
messages: Record<string, PluginRowMessage>;
|
||||
activeTab: "installed" | "discover";
|
||||
searchResults: PluginSearchResult[] | null;
|
||||
applyMutationResult: (result: PluginMutationResult) => void;
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { GatewayRequestError } from "../../api/gateway.ts";
|
||||
import type { ApplicationContext } from "../../app/context.ts";
|
||||
import { i18n } from "../../i18n/index.ts";
|
||||
import { createRuntimeConfigCapability } from "../../lib/config/runtime-config-capability.ts";
|
||||
import type {
|
||||
PluginInstallRequest,
|
||||
PluginListResult,
|
||||
PluginMutationResult,
|
||||
PluginSearchResult,
|
||||
} from "../../lib/plugins/index.ts";
|
||||
import { waitForFast } from "../../test-helpers/wait-for.ts";
|
||||
@@ -199,6 +201,72 @@ describe("PluginsPage", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("retires a pending install-policy review across a same-client reconnect", async () => {
|
||||
const retry = deferred<PluginMutationResult>();
|
||||
let installCalls = 0;
|
||||
const { client, request } = createClient(async (method) => {
|
||||
if (method === "plugins.install") {
|
||||
installCalls += 1;
|
||||
if (installCalls === 1) {
|
||||
throw new GatewayRequestError({
|
||||
code: "INVALID_REQUEST",
|
||||
message: "install requires review",
|
||||
details: {
|
||||
installPolicyCode: "install_policy_warning_acknowledgement_required",
|
||||
targetName: "@openclaw/lobster",
|
||||
targetType: "plugin",
|
||||
requestMode: "install",
|
||||
reason: "Review this plugin.",
|
||||
acknowledgementToken: "approval-token",
|
||||
},
|
||||
});
|
||||
}
|
||||
return retry.promise;
|
||||
}
|
||||
if (method === "plugins.list") {
|
||||
return createResult();
|
||||
}
|
||||
throw new Error(`Unexpected method ${method}`);
|
||||
});
|
||||
const harness = createGateway(client);
|
||||
const { page } = await mountPage(
|
||||
createContext(harness.gateway),
|
||||
createPluginsRouteData(harness.gateway),
|
||||
);
|
||||
const rowKey = "plugin:@openclaw/lobster";
|
||||
const installRequest = {
|
||||
source: "clawhub",
|
||||
packageName: "@openclaw/lobster",
|
||||
} satisfies PluginInstallRequest;
|
||||
|
||||
await page.install(rowKey, installRequest);
|
||||
expect(page.messages[rowKey]?.installPolicyWarning?.details.acknowledgementToken).toBe(
|
||||
"approval-token",
|
||||
);
|
||||
|
||||
const pendingRetry = page.install(rowKey, {
|
||||
...installRequest,
|
||||
installPolicyWarningAcknowledgement: "approval-token",
|
||||
});
|
||||
await waitForFast(() =>
|
||||
expect(request.mock.calls.filter(([method]) => method === "plugins.install")).toHaveLength(2),
|
||||
);
|
||||
expect(page.messages[rowKey]?.installPolicyWarning).toBeDefined();
|
||||
|
||||
harness.emit(client, false);
|
||||
harness.emit(client, true);
|
||||
await page.updateComplete;
|
||||
expect(page.messages[rowKey]).toBeUndefined();
|
||||
|
||||
retry.resolve({
|
||||
ok: true,
|
||||
plugin: createPlugin({ id: "lobster", name: "Lobster" }),
|
||||
restartRequired: true,
|
||||
});
|
||||
await pendingRetry;
|
||||
expect(page.messages[rowKey]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("debounces two-character ClawHub searches and cancels stale input", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { client, request } = createClient(async (method) => {
|
||||
|
||||
@@ -363,6 +363,11 @@ class PluginsPage extends OpenClawLightDomElement {
|
||||
void this.configTask.run([null, this.context.runtimeConfig]);
|
||||
void this.searchTask.run([null, ""]);
|
||||
this.mutationTokens.clear();
|
||||
// A connection-epoch change makes the one-shot approval outcome unknown;
|
||||
// retire its action so reconnect cannot resend a spent or process-stale token.
|
||||
this.messages = Object.fromEntries(
|
||||
Object.entries(this.messages).filter(([, message]) => !message.installPolicyWarning),
|
||||
);
|
||||
}
|
||||
|
||||
private replaceResult(result: PluginListResult | null, preserveIcons = false) {
|
||||
|
||||
@@ -813,6 +813,53 @@ describeControlUiE2e("Control UI Plugins mocked Gateway E2E", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("retires a pending install-policy review after reconnect", async () => {
|
||||
const context = await newContext();
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page, {
|
||||
featureMethods: pluginMethods,
|
||||
methodResponses: pluginMethodResponses(),
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(`${server.baseUrl}settings/plugins`);
|
||||
await page.getByRole("tab", { name: /^Discover/u }).click();
|
||||
const row = page.locator('[data-plugin-id="lobster"]');
|
||||
await row.waitFor({ state: "visible" });
|
||||
|
||||
await gateway.deferNext("plugins.install");
|
||||
await row.getByRole("button", { name: "Install Lobster", exact: true }).click();
|
||||
await gateway.waitForRequest("plugins.install");
|
||||
await gateway.rejectDeferred("plugins.install", {
|
||||
code: "INVALID_REQUEST",
|
||||
message: "install requires review",
|
||||
details: installPolicyWarning,
|
||||
});
|
||||
|
||||
const review = row.getByRole("alert");
|
||||
await review.waitFor({ state: "visible" });
|
||||
const installCountBeforeRetry = (await gateway.getRequests("plugins.install")).length;
|
||||
await gateway.deferNext("plugins.install");
|
||||
await review.getByRole("button", { name: "Install anyway", exact: true }).click();
|
||||
await waitForNextRequest(gateway, "plugins.install", installCountBeforeRetry);
|
||||
await review.getByRole("button", { name: "Installing…", exact: true }).waitFor();
|
||||
|
||||
const socketsBeforeReconnect = await gateway.getSocketCount();
|
||||
await gateway.closeLatest(1001, "install-policy reconnect proof");
|
||||
await expect
|
||||
.poll(() => gateway.getSocketCount(), { timeout: 10_000 })
|
||||
.toBeGreaterThan(socketsBeforeReconnect);
|
||||
|
||||
await review.waitFor({ state: "detached" });
|
||||
await row.getByRole("button", { name: "Install Lobster", exact: true }).waitFor();
|
||||
expect(await row.getByRole("button", { name: "Install anyway", exact: true }).count()).toBe(
|
||||
0,
|
||||
);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps plugin mutations unavailable to read-only operators while browse and search work", async () => {
|
||||
const context = await newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
Reference in New Issue
Block a user