From 5371f7fa8f443447e12ce1cf9a5f83de48587849 Mon Sep 17 00:00:00 2001 From: Andy Pickering Date: Thu, 7 May 2026 13:24:58 +0900 Subject: [PATCH] Add Cypress tests for HITL tool approval flow Co-authored-by: Cursor --- cypress/support/commands.ts | 33 +++++++++++++++ tests/tests/lightspeed-install.cy.ts | 61 ++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index d9f286e0..a046ba2a 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -42,6 +42,8 @@ declare global { query: string, errorMessage: string, ): Chainable; + interceptQueryWithApproval(alias: string, query: string): Chainable; + interceptToolApproval(alias: string, approved: boolean): Chainable; } } } @@ -178,6 +180,37 @@ Cypress.Commands.add( }, ); +/* eslint-disable camelcase */ +const MOCK_STREAMED_RESPONSE_WITH_APPROVAL_BODY = `data: {"event": "start", "data": {"conversation_id": "5f424596-a4f9-4a3a-932b-46a768de3e7c"}} + +data: {"event": "token", "data": {"id": 0, "token": "Mock"}} + +data: {"event": "token", "data": {"id": 1, "token": " response"}} + +data: {"event": "tool_call", "data": {"id": "tool-123", "name": "mock_tool", "args": {"namespace": "default"}}} + +data: {"event": "approval_required", "data": {"approval_id": "abc", "tool_name": "mock_tool", "tool_description": "This action will list pods in the cluster.", "tool_args": {"namespace": "default"}}} + +data: {"event": "end", "data": {"referenced_documents": [], "truncated": false}} +`; +/* eslint-enable camelcase */ + +Cypress.Commands.add('interceptQueryWithApproval', (alias: string, query: string) => { + cy.intercept('POST', getApiUrl('/v1/streaming_query'), (request) => { + expect(request.body.media_type).to.equal('application/json'); + expect(request.body.query).to.include(query); + request.reply({ body: MOCK_STREAMED_RESPONSE_WITH_APPROVAL_BODY, delay: 500 }); + }).as(alias); +}); + +Cypress.Commands.add('interceptToolApproval', (alias: string, approved: boolean) => { + cy.intercept('POST', getApiUrl('/v1/tool-approvals/decision'), (request) => { + expect(request.body.approval_id).to.equal('abc'); + expect(request.body.approved).to.equal(approved); + request.reply({ statusCode: 200, body: {} }); + }).as(alias); +}); + const USER_FEEDBACK_MOCK_RESPONSE = { body: { message: 'Feedback received' } }; Cypress.Commands.add( diff --git a/tests/tests/lightspeed-install.cy.ts b/tests/tests/lightspeed-install.cy.ts index 9a87efb3..2f909503 100644 --- a/tests/tests/lightspeed-install.cy.ts +++ b/tests/tests/lightspeed-install.cy.ts @@ -33,6 +33,8 @@ const userFeedback = `${popover} .ols-plugin__feedback`; const userFeedbackInput = `${userFeedback} textarea`; const userFeedbackSubmit = `${userFeedback} button.pf-m-primary`; const modal = '.ols-plugin__modal'; +const toolApprovalCard = `${popover} .ols-plugin__tool-call`; +const toolLabel = `${popover} .pf-v6-c-label`; const promptArea = `${popover} .ols-plugin__prompt`; const attachButton = `${promptArea} .pf-chatbot__button--attach`; @@ -454,6 +456,65 @@ spec: }); }); + describe('Tool approval (HITL)', { tags: ['@hitl'] }, () => { + it('Test approval card is shown and tool can be approved', () => { + cy.visit('/search/all-namespaces'); + cy.get('h1').contains('Search').should('exist'); + cy.get(mainButton).click(); + + cy.interceptQueryWithApproval('queryWithApproval', PROMPT_SUBMITTED); + cy.interceptToolApproval('approvalStub', true); + cy.get(promptInput).type(`${PROMPT_SUBMITTED}{enter}`); + cy.wait('@queryWithApproval'); + + cy.get(toolApprovalCard).should('exist'); + cy.get(toolApprovalCard).should('contain', 'Review required'); + cy.get(toolApprovalCard).should('contain', 'This action will list pods in the cluster.'); + cy.get(toolApprovalCard).find('button').contains('Approve').should('exist'); + cy.get(toolApprovalCard).find('button').contains('Reject').should('exist'); + + cy.get(toolApprovalCard).contains('View action details').click(); + cy.get(toolApprovalCard).should('contain', 'mock_tool'); + cy.get(toolApprovalCard).should('contain', 'namespace'); + + cy.get(toolApprovalCard).find('button').contains('Approve').click(); + cy.wait('@approvalStub'); + cy.get(toolApprovalCard).should('not.exist'); + cy.get(toolLabel).should('contain', 'mock_tool'); + + cy.get(toolLabel).contains('mock_tool').click(); + cy.get(modal).should('contain', 'Tool output'); + cy.get(modal).should('contain', 'mock_tool'); + cy.get(modal).should('contain', 'Status'); + cy.get(modal).should('contain', 'pending'); + cy.get(modal).should('not.contain', 'Tool call rejected'); + cy.get(modal).find('button[title="Close"]').click(); + }); + + it('Test tool can be rejected', () => { + cy.visit('/search/all-namespaces'); + cy.get('h1').contains('Search').should('exist'); + cy.get(mainButton).click(); + + cy.interceptQueryWithApproval('queryWithApproval', PROMPT_SUBMITTED); + cy.interceptToolApproval('denialStub', false); + cy.get(promptInput).type(`${PROMPT_SUBMITTED}{enter}`); + cy.wait('@queryWithApproval'); + + cy.get(toolApprovalCard).should('exist'); + cy.get(toolApprovalCard).find('button').contains('Reject').click(); + cy.wait('@denialStub'); + cy.get(toolApprovalCard).should('not.exist'); + cy.get(toolLabel).should('contain', 'mock_tool'); + cy.get(toolLabel).contains('mock_tool').click(); + cy.get(modal).should('contain', 'Tool call rejected'); + cy.get(modal).should('contain', 'mock_tool'); + cy.get(modal).should('not.contain', 'Status'); + cy.get(modal).should('not.contain', 'Content'); + cy.get(modal).find('button[title="Close"]').click(); + }); + }); + describe('User feedback', { tags: ['@feedback'] }, () => { it('Test user feedback form', () => { cy.visit('/search/all-namespaces');