[ACS-8398] Unit tests for agents and search ai (#9974)

* ACS-8398 Unit tests for search ai api and agents api

* ACS-8398 Unit tests for getAnswer function from SearchAiApi, corrections for unit tests for SearchAiApi and AgentsApi

* ACS-8398 Unit tests for SearchAiService and AgentService
This commit is contained in:
AleksanderSklorz
2024-07-22 15:56:28 +02:00
committed by Aleksander Sklorz
parent ad9c1a0ce0
commit 16e851e6b5
8 changed files with 648 additions and 3 deletions

View File

@@ -0,0 +1,63 @@
/*!
* @license
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AgentService } from './agent.service';
import { TestBed } from '@angular/core/testing';
import { AgentPaging } from '@alfresco/js-api';
import { ContentTestingModule } from '../../testing/content.testing.module';
describe('AgentService', () => {
let service: AgentService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContentTestingModule]
});
service = TestBed.inject(AgentService);
service.mocked = false;
});
describe('getAgents', () => {
it('should load agents', (done) => {
const paging: AgentPaging = {
list: {
entries: [
{
entry: {
id: '1',
name: 'HR Agent'
}
},
{
entry: {
id: '2',
name: 'Policy Agent'
}
}
]
}
};
spyOn(service.agentsApi, 'getAgents').and.returnValue(Promise.resolve(paging));
service.getAgents().subscribe((pagingResponse) => {
expect(pagingResponse).toBe(paging);
expect(service.agentsApi.getAgents).toHaveBeenCalled();
done();
});
});
});
});

View File

@@ -0,0 +1,255 @@
/*!
* @license
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestBed } from '@angular/core/testing';
import { AiAnswerPaging, Node, QuestionModel, QuestionRequest } from '@alfresco/js-api';
import { ContentTestingModule } from '../../testing/content.testing.module';
import { SearchAiService } from './search-ai.service';
import { SearchAiInputState } from '../models/search-ai-input-state';
import { TranslateService } from '@ngx-translate/core';
describe('SearchAiService', () => {
let service: SearchAiService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ContentTestingModule]
});
service = TestBed.inject(SearchAiService);
service.mocked = false;
});
describe('ask', () => {
it('should load information about question', (done) => {
const question: QuestionModel = {
question: 'some question',
questionId: 'some id',
restrictionQuery: 'node id1,node id 2'
};
spyOn(service.searchAiApi, 'ask').and.returnValue(Promise.resolve([question]));
const questionRequest: QuestionRequest = {
question: 'some question',
nodeIds: ['node id1', 'node id 2']
};
service.ask(questionRequest).subscribe((questionResponse) => {
expect(questionResponse).toBe(question);
expect(service.searchAiApi.ask).toHaveBeenCalledWith([questionRequest]);
done();
});
});
});
describe('getAnswer', () => {
it('should load information about question', (done) => {
const questionId = 'some id';
const answer: AiAnswerPaging = {
list: {
pagination: {
count: 2,
hasMoreItems: false,
skipCount: 0,
maxItems: 100
},
entries: [
{
entry: {
answer: 'Some answer 1',
questionId,
references: [
{
referenceId: 'some reference id 1',
referenceText: 'some reference text 1'
}
]
}
},
{
entry: {
answer: 'Some answer 2',
questionId,
references: [
{
referenceId: 'some reference id 2',
referenceText: 'some reference text 2'
}
]
}
}
]
}
};
spyOn(service.searchAiApi, 'getAnswer').and.returnValue(Promise.resolve(answer));
service.getAnswer(questionId).subscribe((answerResponse) => {
expect(answerResponse).toBe(answer);
expect(service.searchAiApi.getAnswer).toHaveBeenCalledWith(questionId);
done();
});
});
});
describe('updateSearchAiInputState', () => {
it('should trigger toggleSearchAiInput$', () => {
const state: SearchAiInputState = {
active: true,
selectedAgentId: 'some id'
};
service.updateSearchAiInputState(state);
service.toggleSearchAiInput$.subscribe((receivedState) => {
expect(receivedState).toBe(state);
});
});
});
describe('checkSearchAvailability', () => {
let translateService: TranslateService;
const noFilesSelectedError = 'Please select some file.';
const tooManyFilesSelectedError = 'Please select no more than 100 files.';
const nonTextFileSelectedError = 'Only text related files are compatible with AI Agents.';
const folderSelectedError = 'Folders are not compatible with AI Agents.';
beforeEach(() => {
translateService = TestBed.inject(TranslateService);
spyOn(translateService, 'instant').and.callFake((key) => {
switch (key) {
case 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.NO_FILES_SELECTED':
return noFilesSelectedError;
case 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED':
return tooManyFilesSelectedError;
case 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.NON_TEXT_FILE_SELECTED':
return nonTextFileSelectedError;
case 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.FOLDER_SELECTED':
return folderSelectedError;
default:
return '';
}
});
});
it('should return error for no selected nodes', () => {
expect(
service.checkSearchAvailability({
count: 0,
nodes: [],
libraries: [],
isEmpty: true
})
).toBe(noFilesSelectedError);
});
it('should return error for too many files selected', () => {
expect(
service.checkSearchAvailability({
count: 101,
nodes: [],
libraries: [],
isEmpty: false
})
).toBe(tooManyFilesSelectedError);
expect(translateService.instant).toHaveBeenCalledWith('KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED', {
maxFiles: 100,
key: 'KNOWLEDGE_RETRIEVAL.SEARCH.WARNINGS.TOO_MANY_FILES_SELECTED'
});
});
it('should return error for non text file selected if non text file is selected and it is not a folder', () => {
expect(
service.checkSearchAvailability({
count: 1,
nodes: [
{
entry: {
isFolder: false,
content: {
mimeType: 'image/jpeg',
mimeTypeName: 'image/jpeg',
sizeInBytes: 100
}
} as Node
}
],
libraries: [],
isEmpty: false
})
).toBe(nonTextFileSelectedError);
});
it('should not return error for non text file selected if non text mime type node is selected and it is a folder', () => {
expect(
service.checkSearchAvailability({
count: 1,
nodes: [
{
entry: {
isFolder: true,
content: {
mimeType: 'some mime type',
mimeTypeName: 'some mime type',
sizeInBytes: 100
}
} as Node
}
],
libraries: [],
isEmpty: false
})
).not.toBe(nonTextFileSelectedError);
});
it('should return error for folder selected if', () => {
expect(
service.checkSearchAvailability({
count: 1,
nodes: [
{
entry: {
isFolder: true
} as Node
}
],
libraries: [],
isEmpty: false
})
).toBe(folderSelectedError);
});
it('should return more than one error if more validators detected issues', () => {
expect(
service.checkSearchAvailability({
count: 101,
nodes: [
{
entry: {
isFolder: false,
content: {
mimeType: 'image/jpeg',
mimeTypeName: 'image/jpeg',
sizeInBytes: 100
}
} as Node
}
],
libraries: [],
isEmpty: false
})
).toBe(`${tooManyFilesSelectedError} ${nonTextFileSelectedError}`);
});
});
});

View File

@@ -33,9 +33,9 @@ export class SearchAiApi extends BaseApi {
ask(questions: QuestionRequest[]): Promise<QuestionModel[]> {
return this.get({
path: 'questions',
bodyParam: questions.map((question) => ({
...question,
restrictionQuery: question.nodeIds.join(',')
bodyParam: questions.map((questionRequest) => ({
question: questionRequest.question,
restrictionQuery: questionRequest.nodeIds.join(',')
}))
});
}

View File

@@ -0,0 +1,71 @@
/*!
* @license
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AgentMock, EcmAuthMock } from '../mockObjects';
import { AgentsApi, AlfrescoApi } from '../../src';
import assert from 'assert';
describe('AgentsApi', () => {
let agentMock: AgentMock;
let agentsApi: AgentsApi;
beforeEach((done) => {
const hostEcm = 'https://127.0.0.1:8080';
const authResponseMock = new EcmAuthMock(hostEcm);
agentMock = new AgentMock(hostEcm);
authResponseMock.get201Response();
const alfrescoJsApi = new AlfrescoApi({
hostEcm
});
alfrescoJsApi.login('admin', 'admin').then(() => done());
agentsApi = new AgentsApi(alfrescoJsApi);
});
describe('getAgents', () => {
it('should load list of agents', (done) => {
agentMock.mockGetAgents200Response();
agentsApi.getAgents().then((paging) => {
assert.deepStrictEqual(paging, {
list: {
pagination: {
count: 2,
hasMoreItems: false,
skipCount: 0,
maxItems: 100
},
entries: [
{
entry: {
id: 'some id 1',
name: 'some name 1'
}
},
{
entry: {
id: 'some id 2',
name: 'some name 2'
}
}
]
}
});
done();
});
});
});
});

View File

@@ -0,0 +1,116 @@
/*!
* @license
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AlfrescoApi, SearchAiApi } from '../../src';
import { EcmAuthMock, SearchAiMock } from '../mockObjects';
import assert from 'assert';
describe('SearchAiApi', () => {
let searchAiApi: SearchAiApi;
let searchAiMock: SearchAiMock;
beforeEach((done) => {
const hostEcm = 'https://127.0.0.1:8080';
const authResponseMock = new EcmAuthMock(hostEcm);
searchAiMock = new SearchAiMock(hostEcm);
authResponseMock.get201Response();
const alfrescoJsApi = new AlfrescoApi({
hostEcm
});
alfrescoJsApi.login('admin', 'admin').then(() => done());
searchAiApi = new SearchAiApi(alfrescoJsApi);
});
describe('ask', () => {
it('should load question information', (done) => {
searchAiMock.mockGetAsk200Response();
searchAiApi
.ask([
{
question: 'some question 1',
nodeIds: ['some node id 1']
},
{
question: 'some question 2',
nodeIds: ['some node id 2', 'some node id 3']
}
])
.then((questions) => {
assert.deepStrictEqual(questions, [
{
questionId: 'some id 1',
question: 'some question 1',
restrictionQuery: 'some node id 1'
},
{
questionId: 'some id 2',
question: 'some question 2',
restrictionQuery: 'some node id 2,some node id 3'
}
]);
done();
});
});
});
describe('getAnswer', () => {
it('should load question answer', (done) => {
searchAiMock.mockGetAnswer200Response();
searchAiApi.getAnswer('id1').then((answer) => {
assert.deepStrictEqual(answer, {
list: {
pagination: {
count: 2,
hasMoreItems: false,
skipCount: 0,
maxItems: 100
},
entries: [
{
entry: {
answer: 'Some answer 1',
questionId: 'some id 1',
references: [
{
referenceId: 'some reference id 1',
referenceText: 'some reference text 1'
}
]
}
},
{
entry: {
answer: 'Some answer 2',
questionId: 'some id 2',
references: [
{
referenceId: 'some reference id 2',
referenceText: 'some reference text 2'
}
]
}
}
]
}
});
done();
});
});
});
});

View File

@@ -0,0 +1,50 @@
/*!
* @license
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BaseMock } from '../base.mock';
import nock from 'nock';
export class AgentMock extends BaseMock {
mockGetAgents200Response(): void {
nock(this.host, { encodedQueryParams: true })
.get('/alfresco/api/-default-/private/hxi/versions/1/agents')
.reply(200, {
list: {
pagination: {
count: 2,
hasMoreItems: false,
skipCount: 0,
maxItems: 100
},
entries: [
{
entry: {
id: 'some id 1',
name: 'some name 1'
}
},
{
entry: {
id: 'some id 2',
name: 'some name 2'
}
}
]
}
});
}
}

View File

@@ -0,0 +1,88 @@
/*!
* @license
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BaseMock } from '../base.mock';
import nock from 'nock';
export class SearchAiMock extends BaseMock {
mockGetAsk200Response(): void {
nock(this.host, { encodedQueryParams: true })
.get('/alfresco/api/-default-/private/hxi/versions/1/questions', [
{
question: 'some question 1',
restrictionQuery: 'some node id 1'
},
{
question: 'some question 2',
restrictionQuery: 'some node id 2,some node id 3'
}
])
.reply(200, [
{
question: 'some question 1',
questionId: 'some id 1',
restrictionQuery: 'some node id 1'
},
{
question: 'some question 2',
questionId: 'some id 2',
restrictionQuery: 'some node id 2,some node id 3'
}
]);
}
mockGetAnswer200Response(): void {
nock(this.host, { encodedQueryParams: true })
.get('/alfresco/api/-default-/private/hxi/versions/1/answers?questionId=id1')
.reply(200, {
list: {
pagination: {
count: 2,
hasMoreItems: false,
skipCount: 0,
maxItems: 100
},
entries: [
{
entry: {
answer: 'Some answer 1',
questionId: 'some id 1',
references: [
{
referenceId: 'some reference id 1',
referenceText: 'some reference text 1'
}
]
}
},
{
entry: {
answer: 'Some answer 2',
questionId: 'some id 2',
references: [
{
referenceId: 'some reference id 2',
referenceText: 'some reference text 2'
}
]
}
}
]
}
});
}
}

View File

@@ -15,6 +15,7 @@
* limitations under the License.
*/
export * from './content-services/agent.mock';
export * from './content-services/categories.mock';
export * from './content-services/comment.mock';
export * from './content-services/ecm-auth.mock';
@@ -26,6 +27,7 @@ export * from './content-services/groups.mock';
export * from './content-services/find-nodes.mock';
export * from './content-services/rendition.mock';
export * from './content-services/search.mock';
export * from './content-services/search-ai.mock';
export * from './content-services/tag.mock';
export * from './content-services/upload.mock';
export * from './content-services/version.mock';