11/* @vitest -environment jsdom */
22
3- import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
3+ import { afterEach , beforeAll , beforeEach , describe , expect , it , vi } from "vitest" ;
44import { createStorageMock } from "../test-helpers/storage.ts" ;
55import type { ExecApprovalRequest } from "./controllers/exec-approval.ts" ;
66
77type RequestFn = ( method : string , params ?: unknown ) => Promise < unknown > ;
8+ let OpenClawApp : typeof import ( "./app.ts" ) . OpenClawApp ;
89
910function createExecApproval ( overrides : Partial < ExecApprovalRequest > = { } ) : ExecApprovalRequest {
1011 return {
@@ -34,7 +35,6 @@ async function createApp(
3435 request : RequestFn ,
3536 queue : ExecApprovalRequest [ ] = [ createExecApproval ( ) ] ,
3637) {
37- const { OpenClawApp } = await import ( "./app.ts" ) ;
3838 const app = Object . create ( OpenClawApp . prototype ) as InstanceType < typeof OpenClawApp > ;
3939 Object . defineProperties ( app , {
4040 client : { value : { request } , writable : true } ,
@@ -46,6 +46,10 @@ async function createApp(
4646}
4747
4848describe ( "OpenClawApp exec approval decisions" , ( ) => {
49+ beforeAll ( async ( ) => {
50+ ( { OpenClawApp } = await import ( "./app.ts" ) ) ;
51+ } , 20_000 ) ;
52+
4953 beforeEach ( ( ) => {
5054 vi . stubGlobal ( "localStorage" , createStorageMock ( ) ) ;
5155 } ) ;
@@ -70,6 +74,46 @@ describe("OpenClawApp exec approval decisions", () => {
7074 expect ( app . execApprovalBusy ) . toBe ( false ) ;
7175 } ) ;
7276
77+ it ( "keeps the approval prompt on the next queued item after resolving the active item" , async ( ) => {
78+ const request = vi . fn < RequestFn > ( async ( ) => ( { ok : true } ) ) ;
79+ const active = createExecApproval ( { id : "approval-active" , createdAtMs : 2000 } ) ;
80+ const queued = createExecApproval ( {
81+ id : "approval-queued" ,
82+ request : { command : "pnpm test:changed" } ,
83+ createdAtMs : 1000 ,
84+ } ) ;
85+ const app = await createApp ( request , [ active , queued ] ) ;
86+
87+ await app . handleExecApprovalDecision ( "allow-once" ) ;
88+
89+ expect ( request ) . toHaveBeenCalledWith ( "exec.approval.resolve" , {
90+ id : "approval-active" ,
91+ decision : "allow-once" ,
92+ } ) ;
93+ expect ( app . execApprovalQueue ) . toEqual ( [ queued ] ) ;
94+ expect ( app . execApprovalError ) . toBeNull ( ) ;
95+ expect ( app . execApprovalBusy ) . toBe ( false ) ;
96+ } ) ;
97+
98+ it ( "resolves plugin approvals through the plugin approval method" , async ( ) => {
99+ const request = vi . fn < RequestFn > ( async ( ) => ( { ok : true } ) ) ;
100+ const pluginApproval = createExecApproval ( {
101+ id : "plugin-approval-1" ,
102+ kind : "plugin" ,
103+ pluginTitle : "Plugin approval" ,
104+ request : { command : "Plugin approval" } ,
105+ } ) ;
106+ const app = await createApp ( request , [ pluginApproval ] ) ;
107+
108+ await app . handleExecApprovalDecision ( "allow-once" ) ;
109+
110+ expect ( request ) . toHaveBeenCalledWith ( "plugin.approval.resolve" , {
111+ id : "plugin-approval-1" ,
112+ decision : "allow-once" ,
113+ } ) ;
114+ expect ( app . execApprovalQueue ) . toEqual ( [ ] ) ;
115+ } ) ;
116+
73117 it ( "dismisses and refreshes when the backend reports an already resolved approval" , async ( ) => {
74118 const request = vi . fn < RequestFn > ( async ( method ) => {
75119 if ( method === "exec.approval.resolve" ) {
0 commit comments