

















@@ -0,0 +1,113 @@
1+/* @vitest-environment jsdom */
2+3+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4+import { createStorageMock } from "../test-helpers/storage.ts";
5+import type { ExecApprovalRequest } from "./controllers/exec-approval.ts";
6+7+type RequestFn = (method: string, params?: unknown) => Promise<unknown>;
8+9+function createExecApproval(overrides: Partial<ExecApprovalRequest> = {}): ExecApprovalRequest {
10+return {
11+id: "approval-1",
12+kind: "exec",
13+request: { command: "echo hello" },
14+createdAtMs: 1000,
15+expiresAtMs: Date.now() + 60_000,
16+ ...overrides,
17+};
18+}
19+20+function createGatewayError(message: string, details?: unknown): Error {
21+const err = new Error(message);
22+Object.defineProperty(err, "gatewayCode", {
23+value: "INVALID_REQUEST",
24+enumerable: true,
25+});
26+Object.defineProperty(err, "details", {
27+value: details,
28+enumerable: true,
29+});
30+return err;
31+}
32+33+async function createApp(
34+request: RequestFn,
35+queue: ExecApprovalRequest[] = [createExecApproval()],
36+) {
37+const { OpenClawApp } = await import("./app.ts");
38+const app = new OpenClawApp();
39+Object.defineProperty(app, "client", {
40+value: { request },
41+writable: true,
42+});
43+app.execApprovalQueue = queue;
44+app.execApprovalBusy = false;
45+app.execApprovalError = null;
46+return app;
47+}
48+49+describe("OpenClawApp exec approval decisions", () => {
50+beforeEach(() => {
51+vi.stubGlobal("localStorage", createStorageMock());
52+});
53+54+afterEach(() => {
55+vi.unstubAllGlobals();
56+vi.restoreAllMocks();
57+});
58+59+it("dismisses the active approval after same-decision idempotent success", async () => {
60+const request = vi.fn<RequestFn>(async () => ({ ok: true }));
61+const app = await createApp(request);
62+63+await app.handleExecApprovalDecision("allow-once");
64+65+expect(request).toHaveBeenCalledWith("exec.approval.resolve", {
66+id: "approval-1",
67+decision: "allow-once",
68+});
69+expect(app.execApprovalQueue).toEqual([]);
70+expect(app.execApprovalError).toBeNull();
71+expect(app.execApprovalBusy).toBe(false);
72+});
73+74+it("dismisses and refreshes when the backend reports an already resolved approval", async () => {
75+const request = vi.fn<RequestFn>(async (method) => {
76+if (method === "exec.approval.resolve") {
77+throw createGatewayError("approval already resolved", {
78+reason: "APPROVAL_ALREADY_RESOLVED",
79+});
80+}
81+if (method === "exec.approval.list") {
82+return [];
83+}
84+if (method === "plugin.approval.list") {
85+return [];
86+}
87+return {};
88+});
89+const app = await createApp(request);
90+91+await app.handleExecApprovalDecision("deny");
92+93+expect(app.execApprovalQueue).toEqual([]);
94+expect(app.execApprovalError).toBeNull();
95+expect(app.execApprovalBusy).toBe(false);
96+expect(request).toHaveBeenCalledWith("exec.approval.list", {});
97+expect(request).toHaveBeenCalledWith("plugin.approval.list", {});
98+});
99+100+it("keeps the active approval open for unrelated errors", async () => {
101+const request = vi.fn<RequestFn>(async () => {
102+throw createGatewayError("gateway unavailable");
103+});
104+const active = createExecApproval();
105+const app = await createApp(request, [active]);
106+107+await app.handleExecApprovalDecision("deny");
108+109+expect(app.execApprovalQueue).toEqual([active]);
110+expect(app.execApprovalError).toBe("Approval failed: Error: gateway unavailable");
111+expect(app.execApprovalBusy).toBe(false);
112+});
113+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。