Skip to content

Commit e583800

Browse files
Refine AI chatbot registration process (#11)
* improve error popover readability in «DeploymentStatus» Enhance the error popover by adding a «panelStyle» property with word-wrapping. This ensures long text breaks properly, improving readability and preventing layout issues. * improve error handling and enrich contextual information Enhance error propagation and contextual data in HTTP client operations by introducing a detailed «HttpError» utility. Update test cases to validate enriched error messages, including status text and request details. Refactor installation steps to incorporate structured error handling using «StepError», providing detailed context and possible causes for failures. Improve diagnostic messaging during installation, including step-specific details and enriched error descriptions. Add utility functions to extract status codes and derive potential causes for issues, improving debugging capabilities. Update related unit tests to ensure comprehensive coverage of new behaviors. * add rollback support to installation steps Introduce rollback functionality to the installation process, enabling the reversal of completed steps upon failure. Extend «InstallationAIAssistantStep» with a new «rollback» method and update «InstallationProgressManager» to manage rollback execution. Add rollback handling for agent, connector, and model creation steps, ensuring created resources are cleaned up if subsequent steps fail. Enhance integration tests to validate rollback behavior and update the «InstallationResult» structure to include «rollbackErrors» for improved error reporting. * update error message in «TestModelConnectionStep» test Refine the error message in the «TestModelConnectionStep» test to improve clarity and match the updated phrasing. This ensures the test provides more precise feedback when the step validation fails. * remove handling of response body in «HttpError» Simplify the «HttpError» implementation by removing all logic related to determining and handling the presence of a response body. Eliminate the «responseBodyOmitted» property, the «hasMeaningfulBody» function, and associated references. This change reduces complexity and aligns with a security-focused approach by avoiding reliance on response body content. * update tests to validate additional error details Enhance the test for «WindowFetchHttpClient» by adding assertions for «status» and «statusText» in propagated HTTP errors, ensuring more comprehensive error validation. Simplify the failure message validation in «InstallationProgressManager» tests by removing redundant details, improving clarity and focus on the core error message. * refactor formatting and interface definitions Simplify code structure by normalizing formatting for better readability across files. Replace «RollbackError» type with an «interface» for consistency. Update multiline constructs to align with style guidelines. These changes improve code maintainability and adhere to standardized practices. * add rollback handling and error collection in tests Introduce an optional «onRollback» callback to the «TestStep» class, enabling custom rollback logic during test execution. Update the «rollback» method to invoke this callback if provided. Add a new test case to verify proper rollback execution order for failed and completed steps. Ensure rollback errors are collected and validated. Enhances test coverage and ensures consistent behavior during rollback scenarios. * add unit test for «ModelRegister» component Introduce a unit test to verify the behavior of the «ModelRegister» component during installation failure. Mock dependencies like «ModelForm» and «useAssistantInstallation» to simulate scenarios, ensuring proper handling of error states and absence of success messages. * add rollback handling and summary display for installation errors Introduce rollback tracking in the installation process to record reverted steps during failures. Update the response structure to include a «rollbacks» field and propagate this data through the installation workflow. Enhance the UI to display rollback summaries using «addInfoToast» when installation errors occur. Modify tests to verify rollback behavior and ensure accurate reporting of reverted steps. Improve error handling and state management by adding a reference to track the last rollback summary and avoid duplicate toasts. * refactor test imports and rollback methods for consistency Standardize imports across test files by replacing inline React imports with named imports. Simplify code structure for readability by reformatting multiline arrays and conditionals. Remove the «override» keyword from rollback methods in multiple classes to align with TypeScript conventions and ensure compatibility. Improve readability in «derivePossibleCauses» by reformatting conditional checks for network issues. These changes enhance code consistency, maintainability, and readability. * replace named imports with explicit «React» references in mocks Update mocked components and hooks to use explicit «React» references instead of named imports. This change ensures compatibility with Jest's module mocking and avoids potential conflicts with React's internal APIs. Additionally, replace JSX syntax with «React.createElement» in the mocked «MockModelForm» for consistency and clarity in the mock's implementation.
1 parent 3246061 commit e583800

33 files changed

+1075
-50
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright Wazuh Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from 'react';
7+
import userEvent from '@testing-library/user-event';
8+
import { render, screen, waitFor } from '../../../../test/test_utils';
9+
10+
jest.mock('../model-form', () => {
11+
const _React = jest.requireActual<typeof import('react')>('react');
12+
13+
const MockModelForm = ({ onChange, onValidationChange }: any) => {
14+
_React.useEffect(() => {
15+
onChange?.({
16+
modelProvider: 'OpenAI',
17+
model: 'gpt-4o',
18+
apiUrl: 'https://api.openai.com/v1/chat/completions',
19+
apiKey: 'mock-key',
20+
});
21+
onValidationChange?.(true);
22+
}, [onChange, onValidationChange]);
23+
24+
return _React.createElement('div', { 'data-testid': 'mock-model-form' });
25+
};
26+
27+
return {
28+
__esModule: true,
29+
ModelForm: MockModelForm,
30+
};
31+
});
32+
33+
jest.mock('../../modules/installation-manager/hooks/use-assistant-installation', () => {
34+
const React = jest.requireActual<typeof import('react')>('react');
35+
const { ExecutionState } = jest.requireActual<
36+
typeof import('../../modules/installation-manager/domain')
37+
>('../../modules/installation-manager/domain');
38+
39+
const failedStep = {
40+
stepName: 'Create Model',
41+
state: ExecutionState.FAILED,
42+
error: new Error('Model step failed'),
43+
};
44+
45+
const progress = {
46+
getSteps: () => [
47+
{
48+
stepName: 'Persist ML Commons settings',
49+
state: ExecutionState.FINISHED_SUCCESSFULLY,
50+
},
51+
failedStep,
52+
],
53+
getFailedSteps: () => [failedStep],
54+
isFinished: () => false,
55+
isFinishedSuccessfully: () => false,
56+
isFinishedWithWarnings: () => false,
57+
hasFailed: () => true,
58+
};
59+
60+
return {
61+
__esModule: true,
62+
useAssistantInstallation: () => {
63+
const [error, setError] = React.useState<string | undefined>(undefined);
64+
65+
const install = React.useCallback(async () => {
66+
setError('Steps: "Create Model" has failed');
67+
}, []);
68+
69+
return {
70+
install,
71+
setModel: jest.fn(),
72+
reset: jest.fn(),
73+
isLoading: false,
74+
error,
75+
result: { success: false, rollbacks: ['Create Agent', 'Create Connector'] },
76+
modelData: undefined,
77+
progress,
78+
isSuccess: false,
79+
};
80+
},
81+
};
82+
});
83+
84+
import { ModelRegister } from '../model-register';
85+
86+
describe('ModelRegister', () => {
87+
it('does not show success toast when installation fails', async () => {
88+
const user = userEvent.setup();
89+
90+
render(<ModelRegister />);
91+
92+
const deployButton = await screen.findByRole('button', { name: /Deploy/i });
93+
expect(deployButton).toBeEnabled();
94+
95+
await user.click(deployButton);
96+
97+
await screen.findByText(/Error deploying model/i);
98+
99+
await waitFor(() => {
100+
expect(screen.queryByText('Model deployed successfully.')).not.toBeInTheDocument();
101+
});
102+
103+
expect(await screen.findByText(/Rollback summary/i)).toBeInTheDocument();
104+
expect(
105+
await screen.findByText(/Reverted steps: Create Agent, Create Connector/i)
106+
).toBeInTheDocument();
107+
});
108+
});

public/dashboard-assistant/components/deployment-status.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export const DeploymentStatus = ({
9696
<div style={{ display: 'flex', alignItems: 'center' }}>
9797
{uiStatus === StepStatus.ERROR && step?.error ? (
9898
<EuiPopover
99+
panelStyle={{ wordBreak: 'break-word' }}
99100
isOpen={openErrorPopoverKey === key}
100101
closePopover={() => setOpenErrorPopoverKey(null)}
101102
anchorPosition="rightCenter"

public/dashboard-assistant/components/model-register.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import React, { useState, useCallback, useEffect } from 'react';
6+
import React, { useState, useCallback, useEffect, useRef } from 'react';
77
import {
88
EuiButton,
99
EuiButtonEmpty,
@@ -50,15 +50,17 @@ const ModelRegisterComponent = ({
5050
onDeployed,
5151
}: ModelRegisterProps) => {
5252
const [isDeployed, setIsDeployed] = useState(false);
53-
const { addSuccessToast, addErrorToast } = useToast();
53+
const { addSuccessToast, addErrorToast, addInfoToast } = useToast();
5454
const {
5555
install: startInstallationProcess,
5656
setModel,
5757
isLoading: isInstalling,
5858
error: installationError,
5959
progress: installationProgress,
6060
isSuccess: isInstallationSuccessful,
61+
result: installationResult,
6162
} = useAssistantInstallation();
63+
const lastRollbackKeyRef = useRef<string | null>(null);
6264

6365
useEffect(() => {
6466
if (installationError) {
@@ -67,7 +69,19 @@ const ModelRegisterComponent = ({
6769
`${installationError}. Rolling back current installation. Please, verify data provided and try again.`
6870
);
6971
}
70-
}, [addErrorToast, addSuccessToast, installationError]);
72+
}, [addErrorToast, installationError]);
73+
74+
useEffect(() => {
75+
if (installationError && installationResult?.rollbacks?.length) {
76+
const rollbackSummary = installationResult.rollbacks.join(', ');
77+
if (lastRollbackKeyRef.current !== rollbackSummary) {
78+
addInfoToast('Rollback summary', `Reverted steps: ${rollbackSummary}`);
79+
lastRollbackKeyRef.current = rollbackSummary;
80+
}
81+
} else if (!installationError) {
82+
lastRollbackKeyRef.current = null;
83+
}
84+
}, [addInfoToast, installationError, installationResult]);
7185

7286
useEffect(() => {
7387
if (isInstallationSuccessful) {

public/dashboard-assistant/modules/agent/application/ports/__mocks__/agent-repository.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { AgentRepository } from '../agent-repository';
88
export function createAgentRepositoryMock(): jest.Mocked<AgentRepository> {
99
return {
1010
create: jest.fn(),
11+
delete: jest.fn(),
1112
execute: jest.fn(),
1213
getActive: jest.fn(),
1314
register: jest.fn(),

public/dashboard-assistant/modules/agent/application/ports/agent-repository.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { CreateRepository } from '../../../common/domain/entities/repository';
6+
import { CreateRepository, DeleteRepository } from '../../../common/domain/entities/repository';
77
import { Agent } from '../../domain/entities/agent';
88
import { CreateAgentDto } from '../dtos/create-agent-dto';
99

10-
export interface AgentRepository extends CreateRepository<Agent, CreateAgentDto> {
10+
export interface AgentRepository extends CreateRepository<Agent, CreateAgentDto>, DeleteRepository {
1111
execute(id: string, parameters: any): Promise<any>;
1212
getActive(): Promise<string | undefined>;
1313
register(agentId: string): Promise<void>;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* Copyright Wazuh Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import type { AgentRepository } from '../ports/agent-repository';
7+
8+
export const deleteAgentUseCase = (agentRepository: AgentRepository) => async (
9+
agentId: string
10+
): Promise<void> => {
11+
await agentRepository.delete(agentId);
12+
};
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright Wazuh Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
const SENSITIVE_KEY_PATTERN = /(key|token|secret|password|authorization)$/i;
7+
const MAX_BODY_LENGTH = 1000;
8+
9+
export class HttpError extends Error {
10+
public response?: Response;
11+
public status?: number;
12+
public statusText?: string;
13+
public request?: { method: string; url: string; body?: string };
14+
15+
private constructor(message: string) {
16+
super(message);
17+
this.name = 'HttpError';
18+
}
19+
20+
public static async create(
21+
response: Response,
22+
options: RequestInit & { method: string },
23+
url: string
24+
): Promise<HttpError> {
25+
const method = options.method.toUpperCase();
26+
const sanitizedUrl = this.sanitizeUrl(url);
27+
28+
const statusText = response.statusText?.trim();
29+
const statusSummary = statusText ? `${response.status} ${statusText}` : `${response.status}`;
30+
31+
const message = this.buildFailureMessage({
32+
method,
33+
url: sanitizedUrl,
34+
statusSummary,
35+
});
36+
37+
const error = new HttpError(message);
38+
error.response = response;
39+
error.status = response.status;
40+
error.statusText = response.statusText;
41+
error.request = {
42+
method,
43+
url: sanitizedUrl,
44+
body: this.sanitizeRequestBody(options.body ?? undefined),
45+
};
46+
47+
return error;
48+
}
49+
50+
private static buildFailureMessage(params: {
51+
method: string;
52+
url: string;
53+
statusSummary: string;
54+
}): string {
55+
const { method, url, statusSummary } = params;
56+
57+
const messageParts = [`HTTP ${method} ${url} failed with status ${statusSummary}`].filter(
58+
Boolean
59+
);
60+
61+
return messageParts.join('. ');
62+
}
63+
64+
private static sanitizeUrl(url: string): string {
65+
try {
66+
const base =
67+
typeof window !== 'undefined' && window.location?.origin
68+
? window.location.origin
69+
: 'http://localhost';
70+
const parsed = new URL(url, base);
71+
parsed.searchParams.forEach((value, key) => {
72+
if (SENSITIVE_KEY_PATTERN.test(key)) {
73+
parsed.searchParams.delete(key);
74+
}
75+
});
76+
77+
if (!url.startsWith('http')) {
78+
return `${parsed.pathname}${parsed.search}${parsed.hash}` || parsed.pathname;
79+
}
80+
81+
return parsed.toString();
82+
} catch (error) {
83+
return url;
84+
}
85+
}
86+
87+
private static sanitizeStructuredValue(value: unknown): unknown {
88+
if (value == null) {
89+
return value;
90+
}
91+
92+
if (Array.isArray(value)) {
93+
return value.map((item) => this.sanitizeStructuredValue(item));
94+
}
95+
96+
if (typeof value === 'object') {
97+
return Object.entries(value as Record<string, unknown>).reduce<Record<string, unknown>>(
98+
(acc, [key, entryValue]) => {
99+
if (typeof entryValue !== 'string' || !SENSITIVE_KEY_PATTERN.test(key)) {
100+
acc[key] = this.sanitizeStructuredValue(entryValue);
101+
}
102+
return acc;
103+
},
104+
{}
105+
);
106+
}
107+
108+
return value;
109+
}
110+
111+
private static sanitizeRequestBody(body?: BodyInit | null): string | undefined {
112+
if (!body) {
113+
return undefined;
114+
}
115+
116+
if (typeof body === 'string') {
117+
try {
118+
const parsed = JSON.parse(body);
119+
const sanitized = this.sanitizeStructuredValue(parsed);
120+
return JSON.stringify(sanitized);
121+
} catch (error) {
122+
return body.length > MAX_BODY_LENGTH ? `${body.slice(0, MAX_BODY_LENGTH)}...` : body;
123+
}
124+
}
125+
126+
return '';
127+
}
128+
}

0 commit comments

Comments
 (0)