* fix: #3693 broken thread.json should not break the entire threads * test: add tests
This commit is contained in:
parent
c5e0c93ab4
commit
aee8624338
5
extensions/conversational-extension/jest.config.js
Normal file
5
extensions/conversational-extension/jest.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
}
|
||||
@ -7,6 +7,7 @@
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"build:publish": "rimraf *.tgz --glob && yarn build && npm pack && cpx *.tgz ../../pre-install"
|
||||
},
|
||||
|
||||
408
extensions/conversational-extension/src/Conversational.test.ts
Normal file
408
extensions/conversational-extension/src/Conversational.test.ts
Normal file
@ -0,0 +1,408 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
jest.mock('@janhq/core', () => ({
|
||||
...jest.requireActual('@janhq/core/node'),
|
||||
fs: {
|
||||
existsSync: jest.fn(),
|
||||
mkdir: jest.fn(),
|
||||
writeFileSync: jest.fn(),
|
||||
readdirSync: jest.fn(),
|
||||
readFileSync: jest.fn(),
|
||||
appendFileSync: jest.fn(),
|
||||
rm: jest.fn(),
|
||||
writeBlob: jest.fn(),
|
||||
joinPath: jest.fn(),
|
||||
fileStat: jest.fn(),
|
||||
},
|
||||
joinPath: jest.fn(),
|
||||
ConversationalExtension: jest.fn(),
|
||||
}))
|
||||
|
||||
import { fs } from '@janhq/core'
|
||||
|
||||
import JSONConversationalExtension from '.'
|
||||
|
||||
describe('JSONConversationalExtension Tests', () => {
|
||||
let extension: JSONConversationalExtension
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
extension = new JSONConversationalExtension()
|
||||
})
|
||||
|
||||
it('should create thread folder on load if it does not exist', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(fs, 'existsSync').mockResolvedValue(false)
|
||||
const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({})
|
||||
|
||||
await extension.onLoad()
|
||||
|
||||
expect(mkdirSpy).toHaveBeenCalledWith('file://threads')
|
||||
})
|
||||
|
||||
it('should log message on unload', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'debug').mockImplementation()
|
||||
|
||||
extension.onUnload()
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'JSONConversationalExtension unloaded'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return sorted threads', async () => {
|
||||
jest
|
||||
.spyOn(extension, 'getValidThreadDirs')
|
||||
.mockResolvedValue(['dir1', 'dir2'])
|
||||
jest
|
||||
.spyOn(extension, 'readThread')
|
||||
.mockResolvedValueOnce({ updated: '2023-01-01' })
|
||||
.mockResolvedValueOnce({ updated: '2023-01-02' })
|
||||
|
||||
const threads = await extension.getThreads()
|
||||
|
||||
expect(threads).toEqual([
|
||||
{ updated: '2023-01-02' },
|
||||
{ updated: '2023-01-01' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should ignore broken threads', async () => {
|
||||
jest
|
||||
.spyOn(extension, 'getValidThreadDirs')
|
||||
.mockResolvedValue(['dir1', 'dir2'])
|
||||
jest
|
||||
.spyOn(extension, 'readThread')
|
||||
.mockResolvedValueOnce(JSON.stringify({ updated: '2023-01-01' }))
|
||||
.mockResolvedValueOnce('this_is_an_invalid_json_content')
|
||||
|
||||
const threads = await extension.getThreads()
|
||||
|
||||
expect(threads).toEqual([{ updated: '2023-01-01' }])
|
||||
})
|
||||
|
||||
it('should save thread', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(fs, 'existsSync').mockResolvedValue(false)
|
||||
const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({})
|
||||
const writeFileSyncSpy = jest
|
||||
.spyOn(fs, 'writeFileSync')
|
||||
.mockResolvedValue({})
|
||||
|
||||
const thread = { id: '1', updated: '2023-01-01' } as any
|
||||
await extension.saveThread(thread)
|
||||
|
||||
expect(mkdirSpy).toHaveBeenCalled()
|
||||
expect(writeFileSyncSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should delete thread', async () => {
|
||||
const rmSpy = jest.spyOn(fs, 'rm').mockResolvedValue({})
|
||||
|
||||
await extension.deleteThread('1')
|
||||
|
||||
expect(rmSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should add new message', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(fs, 'existsSync').mockResolvedValue(false)
|
||||
const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({})
|
||||
const appendFileSyncSpy = jest
|
||||
.spyOn(fs, 'appendFileSync')
|
||||
.mockResolvedValue({})
|
||||
|
||||
const message = {
|
||||
thread_id: '1',
|
||||
content: [{ type: 'text', text: { annotations: [] } }],
|
||||
} as any
|
||||
await extension.addNewMessage(message)
|
||||
|
||||
expect(mkdirSpy).toHaveBeenCalled()
|
||||
expect(appendFileSyncSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should store image', async () => {
|
||||
const writeBlobSpy = jest.spyOn(fs, 'writeBlob').mockResolvedValue({})
|
||||
|
||||
await extension.storeImage(
|
||||
'data:image/png;base64,abcd',
|
||||
'path/to/image.png'
|
||||
)
|
||||
|
||||
expect(writeBlobSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should store file', async () => {
|
||||
const writeBlobSpy = jest.spyOn(fs, 'writeBlob').mockResolvedValue({})
|
||||
|
||||
await extension.storeFile(
|
||||
'data:application/pdf;base64,abcd',
|
||||
'path/to/file.pdf'
|
||||
)
|
||||
|
||||
expect(writeBlobSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should write messages', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(fs, 'existsSync').mockResolvedValue(false)
|
||||
const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({})
|
||||
const writeFileSyncSpy = jest
|
||||
.spyOn(fs, 'writeFileSync')
|
||||
.mockResolvedValue({})
|
||||
|
||||
const messages = [{ id: '1', thread_id: '1', content: [] }] as any
|
||||
await extension.writeMessages('1', messages)
|
||||
|
||||
expect(mkdirSpy).toHaveBeenCalled()
|
||||
expect(writeFileSyncSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should get all messages on string response', async () => {
|
||||
jest.spyOn(fs, 'readdirSync').mockResolvedValue(['messages.jsonl'])
|
||||
jest.spyOn(fs, 'readFileSync').mockResolvedValue('{"id":"1"}\n{"id":"2"}\n')
|
||||
|
||||
const messages = await extension.getAllMessages('1')
|
||||
|
||||
expect(messages).toEqual([{ id: '1' }, { id: '2' }])
|
||||
})
|
||||
|
||||
it('should get all messages on object response', async () => {
|
||||
jest.spyOn(fs, 'readdirSync').mockResolvedValue(['messages.jsonl'])
|
||||
jest.spyOn(fs, 'readFileSync').mockResolvedValue({ id: 1 })
|
||||
|
||||
const messages = await extension.getAllMessages('1')
|
||||
|
||||
expect(messages).toEqual([{ id: 1 }])
|
||||
})
|
||||
|
||||
it('get all messages return empty on error', async () => {
|
||||
jest.spyOn(fs, 'readdirSync').mockRejectedValue(['messages.jsonl'])
|
||||
|
||||
const messages = await extension.getAllMessages('1')
|
||||
|
||||
expect(messages).toEqual([])
|
||||
})
|
||||
|
||||
it('return empty messages on no messages file', async () => {
|
||||
jest.spyOn(fs, 'readdirSync').mockResolvedValue([])
|
||||
|
||||
const messages = await extension.getAllMessages('1')
|
||||
|
||||
expect(messages).toEqual([])
|
||||
})
|
||||
|
||||
it('should ignore error message', async () => {
|
||||
jest.spyOn(fs, 'readdirSync').mockResolvedValue(['messages.jsonl'])
|
||||
jest
|
||||
.spyOn(fs, 'readFileSync')
|
||||
.mockResolvedValue('{"id":"1"}\nyolo\n{"id":"2"}\n')
|
||||
|
||||
const messages = await extension.getAllMessages('1')
|
||||
|
||||
expect(messages).toEqual([{ id: '1' }, { id: '2' }])
|
||||
})
|
||||
|
||||
it('should create thread folder on load if it does not exist', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(fs, 'existsSync').mockResolvedValue(false)
|
||||
const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({})
|
||||
|
||||
await extension.onLoad()
|
||||
|
||||
expect(mkdirSpy).toHaveBeenCalledWith('file://threads')
|
||||
})
|
||||
|
||||
it('should log message on unload', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'debug').mockImplementation()
|
||||
|
||||
extension.onUnload()
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'JSONConversationalExtension unloaded'
|
||||
)
|
||||
})
|
||||
|
||||
it('should return sorted threads', async () => {
|
||||
jest
|
||||
.spyOn(extension, 'getValidThreadDirs')
|
||||
.mockResolvedValue(['dir1', 'dir2'])
|
||||
jest
|
||||
.spyOn(extension, 'readThread')
|
||||
.mockResolvedValueOnce({ updated: '2023-01-01' })
|
||||
.mockResolvedValueOnce({ updated: '2023-01-02' })
|
||||
|
||||
const threads = await extension.getThreads()
|
||||
|
||||
expect(threads).toEqual([
|
||||
{ updated: '2023-01-02' },
|
||||
{ updated: '2023-01-01' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should ignore broken threads', async () => {
|
||||
jest
|
||||
.spyOn(extension, 'getValidThreadDirs')
|
||||
.mockResolvedValue(['dir1', 'dir2'])
|
||||
jest
|
||||
.spyOn(extension, 'readThread')
|
||||
.mockResolvedValueOnce(JSON.stringify({ updated: '2023-01-01' }))
|
||||
.mockResolvedValueOnce('this_is_an_invalid_json_content')
|
||||
|
||||
const threads = await extension.getThreads()
|
||||
|
||||
expect(threads).toEqual([{ updated: '2023-01-01' }])
|
||||
})
|
||||
|
||||
it('should save thread', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(fs, 'existsSync').mockResolvedValue(false)
|
||||
const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({})
|
||||
const writeFileSyncSpy = jest
|
||||
.spyOn(fs, 'writeFileSync')
|
||||
.mockResolvedValue({})
|
||||
|
||||
const thread = { id: '1', updated: '2023-01-01' } as any
|
||||
await extension.saveThread(thread)
|
||||
|
||||
expect(mkdirSpy).toHaveBeenCalled()
|
||||
expect(writeFileSyncSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should delete thread', async () => {
|
||||
const rmSpy = jest.spyOn(fs, 'rm').mockResolvedValue({})
|
||||
|
||||
await extension.deleteThread('1')
|
||||
|
||||
expect(rmSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should add new message', async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(fs, 'existsSync').mockResolvedValue(false)
|
||||
const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({})
|
||||
const appendFileSyncSpy = jest
|
||||
.spyOn(fs, 'appendFileSync')
|
||||
.mockResolvedValue({})
|
||||
|
||||
const message = {
|
||||
thread_id: '1',
|
||||
content: [{ type: 'text', text: { annotations: [] } }],
|
||||
} as any
|
||||
await extension.addNewMessage(message)
|
||||
|
||||
expect(mkdirSpy).toHaveBeenCalled()
|
||||
expect(appendFileSyncSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should add new image message', async () => {
|
||||
jest
|
||||
.spyOn(fs, 'existsSync')
|
||||
// @ts-ignore
|
||||
.mockResolvedValueOnce(false)
|
||||
// @ts-ignore
|
||||
.mockResolvedValueOnce(false)
|
||||
// @ts-ignore
|
||||
.mockResolvedValueOnce(true)
|
||||
const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({})
|
||||
const appendFileSyncSpy = jest
|
||||
.spyOn(fs, 'appendFileSync')
|
||||
.mockResolvedValue({})
|
||||
jest.spyOn(fs, 'writeBlob').mockResolvedValue({})
|
||||
|
||||
const message = {
|
||||
thread_id: '1',
|
||||
content: [
|
||||
{ type: 'image', text: { annotations: ['data:image;base64,hehe'] } },
|
||||
],
|
||||
} as any
|
||||
await extension.addNewMessage(message)
|
||||
|
||||
expect(mkdirSpy).toHaveBeenCalled()
|
||||
expect(appendFileSyncSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should add new pdf message', async () => {
|
||||
jest
|
||||
.spyOn(fs, 'existsSync')
|
||||
// @ts-ignore
|
||||
.mockResolvedValueOnce(false)
|
||||
// @ts-ignore
|
||||
.mockResolvedValueOnce(false)
|
||||
// @ts-ignore
|
||||
.mockResolvedValueOnce(true)
|
||||
const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({})
|
||||
const appendFileSyncSpy = jest
|
||||
.spyOn(fs, 'appendFileSync')
|
||||
.mockResolvedValue({})
|
||||
jest.spyOn(fs, 'writeBlob').mockResolvedValue({})
|
||||
|
||||
const message = {
|
||||
thread_id: '1',
|
||||
content: [
|
||||
{ type: 'pdf', text: { annotations: ['data:pdf;base64,hehe'] } },
|
||||
],
|
||||
} as any
|
||||
await extension.addNewMessage(message)
|
||||
|
||||
expect(mkdirSpy).toHaveBeenCalled()
|
||||
expect(appendFileSyncSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should store image', async () => {
|
||||
const writeBlobSpy = jest.spyOn(fs, 'writeBlob').mockResolvedValue({})
|
||||
|
||||
await extension.storeImage(
|
||||
'data:image/png;base64,abcd',
|
||||
'path/to/image.png'
|
||||
)
|
||||
|
||||
expect(writeBlobSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should store file', async () => {
|
||||
const writeBlobSpy = jest.spyOn(fs, 'writeBlob').mockResolvedValue({})
|
||||
|
||||
await extension.storeFile(
|
||||
'data:application/pdf;base64,abcd',
|
||||
'path/to/file.pdf'
|
||||
)
|
||||
|
||||
expect(writeBlobSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('test readThread', () => {
|
||||
let extension: JSONConversationalExtension
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
extension = new JSONConversationalExtension()
|
||||
})
|
||||
|
||||
it('should read thread', async () => {
|
||||
jest
|
||||
.spyOn(fs, 'readFileSync')
|
||||
.mockResolvedValue(JSON.stringify({ id: '1' }))
|
||||
const thread = await extension.readThread('1')
|
||||
expect(thread).toEqual(`{"id":"1"}`)
|
||||
})
|
||||
|
||||
it('getValidThreadDirs should return valid thread directories', async () => {
|
||||
jest
|
||||
.spyOn(fs, 'readdirSync')
|
||||
.mockResolvedValueOnce(['1', '2', '3'])
|
||||
.mockResolvedValueOnce(['thread.json'])
|
||||
.mockResolvedValueOnce(['thread.json'])
|
||||
.mockResolvedValueOnce([])
|
||||
// @ts-ignore
|
||||
jest.spyOn(fs, 'existsSync').mockResolvedValue(true)
|
||||
jest.spyOn(fs, 'fileStat').mockResolvedValue({
|
||||
isDirectory: true,
|
||||
} as any)
|
||||
const validThreadDirs = await extension.getValidThreadDirs()
|
||||
expect(validThreadDirs).toEqual(['1', '2'])
|
||||
})
|
||||
})
|
||||
@ -5,6 +5,7 @@ import {
|
||||
Thread,
|
||||
ThreadMessage,
|
||||
} from '@janhq/core'
|
||||
import { safelyParseJSON } from './jsonUtil'
|
||||
|
||||
/**
|
||||
* JSONConversationalExtension is a ConversationalExtension implementation that provides
|
||||
@ -45,10 +46,11 @@ export default class JSONConversationalExtension extends ConversationalExtension
|
||||
if (result.status === 'fulfilled') {
|
||||
return typeof result.value === 'object'
|
||||
? result.value
|
||||
: JSON.parse(result.value)
|
||||
: safelyParseJSON(result.value)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
.filter((convo) => convo != null)
|
||||
.filter((convo) => !!convo)
|
||||
convos.sort(
|
||||
(a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime()
|
||||
)
|
||||
@ -195,7 +197,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
|
||||
* @param threadDirName the thread dir we are reading from.
|
||||
* @returns data of the thread
|
||||
*/
|
||||
private async readThread(threadDirName: string): Promise<any> {
|
||||
async readThread(threadDirName: string): Promise<any> {
|
||||
return fs.readFileSync(
|
||||
await joinPath([
|
||||
JSONConversationalExtension._threadFolder,
|
||||
@ -210,7 +212,7 @@ export default class JSONConversationalExtension extends ConversationalExtension
|
||||
* Returns a Promise that resolves to an array of thread directories.
|
||||
* @private
|
||||
*/
|
||||
private async getValidThreadDirs(): Promise<string[]> {
|
||||
async getValidThreadDirs(): Promise<string[]> {
|
||||
const fileInsideThread: string[] = await fs.readdirSync(
|
||||
JSONConversationalExtension._threadFolder
|
||||
)
|
||||
@ -266,7 +268,8 @@ export default class JSONConversationalExtension extends ConversationalExtension
|
||||
|
||||
const messages: ThreadMessage[] = []
|
||||
result.forEach((line: string) => {
|
||||
messages.push(JSON.parse(line))
|
||||
const message = safelyParseJSON(line)
|
||||
if (message) messages.push(safelyParseJSON(line))
|
||||
})
|
||||
return messages
|
||||
} catch (err) {
|
||||
|
||||
14
extensions/conversational-extension/src/jsonUtil.ts
Normal file
14
extensions/conversational-extension/src/jsonUtil.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// Note about performance
|
||||
// The v8 JavaScript engine used by Node.js cannot optimise functions which contain a try/catch block.
|
||||
// v8 4.5 and above can optimise try/catch
|
||||
export function safelyParseJSON(json) {
|
||||
// This function cannot be optimised, it's best to
|
||||
// keep it small!
|
||||
var parsed
|
||||
try {
|
||||
parsed = JSON.parse(json)
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
return parsed // Could be undefined!
|
||||
}
|
||||
@ -10,5 +10,6 @@
|
||||
"skipLibCheck": true,
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["./src"]
|
||||
"include": ["./src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
|
||||
87
extensions/model-extension/src/helpers/path.test.ts
Normal file
87
extensions/model-extension/src/helpers/path.test.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { extractFileName } from './path';
|
||||
|
||||
describe('extractFileName Function', () => {
|
||||
it('should correctly extract the file name with the provided file extension', () => {
|
||||
const url = 'http://example.com/some/path/to/file.ext';
|
||||
const fileExtension = '.ext';
|
||||
const fileName = extractFileName(url, fileExtension);
|
||||
expect(fileName).toBe('file.ext');
|
||||
});
|
||||
|
||||
it('should correctly append the file extension if it does not already exist in the file name', () => {
|
||||
const url = 'http://example.com/some/path/to/file';
|
||||
const fileExtension = '.txt';
|
||||
const fileName = extractFileName(url, fileExtension);
|
||||
expect(fileName).toBe('file.txt');
|
||||
});
|
||||
|
||||
it('should handle cases where the URL does not have a file extension correctly', () => {
|
||||
const url = 'http://example.com/some/path/to/file';
|
||||
const fileExtension = '.jpg';
|
||||
const fileName = extractFileName(url, fileExtension);
|
||||
expect(fileName).toBe('file.jpg');
|
||||
});
|
||||
|
||||
it('should correctly handle URLs without a trailing slash', () => {
|
||||
const url = 'http://example.com/some/path/tofile';
|
||||
const fileExtension = '.txt';
|
||||
const fileName = extractFileName(url, fileExtension);
|
||||
expect(fileName).toBe('tofile.txt');
|
||||
});
|
||||
|
||||
it('should correctly handle URLs with multiple file extensions', () => {
|
||||
const url = 'http://example.com/some/path/tofile.tar.gz';
|
||||
const fileExtension = '.gz';
|
||||
const fileName = extractFileName(url, fileExtension);
|
||||
expect(fileName).toBe('tofile.tar.gz');
|
||||
});
|
||||
|
||||
it('should correctly handle URLs with special characters', () => {
|
||||
const url = 'http://example.com/some/path/tófílë.extë';
|
||||
const fileExtension = '.extë';
|
||||
const fileName = extractFileName(url, fileExtension);
|
||||
expect(fileName).toBe('tófílë.extë');
|
||||
});
|
||||
|
||||
it('should correctly handle URLs that are just a file with no path', () => {
|
||||
const url = 'http://example.com/file.txt';
|
||||
const fileExtension = '.txt';
|
||||
const fileName = extractFileName(url, fileExtension);
|
||||
expect(fileName).toBe('file.txt');
|
||||
});
|
||||
|
||||
it('should correctly handle URLs that have special query parameters', () => {
|
||||
const url = 'http://example.com/some/path/tofile.ext?query=1';
|
||||
const fileExtension = '.ext';
|
||||
const fileName = extractFileName(url.split('?')[0], fileExtension);
|
||||
expect(fileName).toBe('tofile.ext');
|
||||
});
|
||||
|
||||
it('should correctly handle URLs that have uppercase characters', () => {
|
||||
const url = 'http://EXAMPLE.COM/PATH/TO/FILE.EXT';
|
||||
const fileExtension = '.ext';
|
||||
const fileName = extractFileName(url, fileExtension);
|
||||
expect(fileName).toBe('FILE.EXT');
|
||||
});
|
||||
|
||||
it('should correctly handle invalid URLs', () => {
|
||||
const url = 'invalid-url';
|
||||
const fileExtension = '.txt';
|
||||
const fileName = extractFileName(url, fileExtension);
|
||||
expect(fileName).toBe('invalid-url.txt');
|
||||
});
|
||||
|
||||
it('should correctly handle empty URLs', () => {
|
||||
const url = '';
|
||||
const fileExtension = '.txt';
|
||||
const fileName = extractFileName(url, fileExtension);
|
||||
expect(fileName).toBe('.txt');
|
||||
});
|
||||
|
||||
it('should correctly handle undefined URLs', () => {
|
||||
const url = undefined;
|
||||
const fileExtension = '.txt';
|
||||
const fileName = extractFileName(url as any, fileExtension);
|
||||
expect(fileName).toBe('.txt');
|
||||
});
|
||||
});
|
||||
@ -3,6 +3,8 @@
|
||||
*/
|
||||
|
||||
export function extractFileName(url: string, fileExtension: string): string {
|
||||
if(!url) return fileExtension
|
||||
|
||||
const extractedFileName = url.split('/').pop()
|
||||
const fileName = extractedFileName.toLowerCase().endsWith(fileExtension)
|
||||
? extractedFileName
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
module.exports = {
|
||||
projects: ['<rootDir>/core', '<rootDir>/web', '<rootDir>/joi'],
|
||||
projects: [
|
||||
'<rootDir>/core',
|
||||
'<rootDir>/web',
|
||||
'<rootDir>/joi',
|
||||
'<rootDir>/extensions/inference-nitro-extension',
|
||||
'<rootDir>/extensions/conversational-extension',
|
||||
'<rootDir>/extensions/model-extension',
|
||||
],
|
||||
}
|
||||
|
||||
107
web/containers/ErrorMessage/index.test.tsx
Normal file
107
web/containers/ErrorMessage/index.test.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
// ErrorMessage.test.tsx
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ErrorMessage from './index';
|
||||
import { ThreadMessage, MessageStatus, ErrorCode } from '@janhq/core';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import useSendChatMessage from '@/hooks/useSendChatMessage';
|
||||
|
||||
// Mock the dependencies
|
||||
jest.mock('jotai', () => {
|
||||
const originalModule = jest.requireActual('jotai')
|
||||
return {
|
||||
...originalModule,
|
||||
useAtomValue: jest.fn(),
|
||||
useSetAtom: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/hooks/useSendChatMessage', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('ErrorMessage Component', () => {
|
||||
const mockSetMainState = jest.fn();
|
||||
const mockSetSelectedSettingScreen = jest.fn();
|
||||
const mockSetModalTroubleShooting = jest.fn();
|
||||
const mockResendChatMessage = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useAtomValue as jest.Mock).mockReturnValue([]);
|
||||
(useSetAtom as jest.Mock).mockReturnValue(mockSetMainState);
|
||||
(useSetAtom as jest.Mock).mockReturnValue(mockSetSelectedSettingScreen);
|
||||
(useSetAtom as jest.Mock).mockReturnValue(mockSetModalTroubleShooting);
|
||||
(useSendChatMessage as jest.Mock).mockReturnValue({ resendChatMessage: mockResendChatMessage });
|
||||
});
|
||||
|
||||
it('renders stopped message correctly', () => {
|
||||
const message: ThreadMessage = {
|
||||
id: '1',
|
||||
status: MessageStatus.Stopped,
|
||||
content: [{ text: { value: 'Test message' } }],
|
||||
} as ThreadMessage;
|
||||
|
||||
render(<ErrorMessage message={message} />);
|
||||
|
||||
expect(screen.getByText("Oops! The generation was interrupted. Let's give it another go!")).toBeInTheDocument();
|
||||
expect(screen.getByText('Regenerate')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders error message with InvalidApiKey correctly', () => {
|
||||
const message: ThreadMessage = {
|
||||
id: '1',
|
||||
status: MessageStatus.Error,
|
||||
error_code: ErrorCode.InvalidApiKey,
|
||||
content: [{ text: { value: 'Invalid API Key' } }],
|
||||
} as ThreadMessage;
|
||||
|
||||
render(<ErrorMessage message={message} />);
|
||||
|
||||
expect(screen.getByTestId('invalid-API-key-error')).toBeInTheDocument();
|
||||
expect(screen.getByText('Settings')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders general error message correctly', () => {
|
||||
const message: ThreadMessage = {
|
||||
id: '1',
|
||||
status: MessageStatus.Error,
|
||||
error_code: ErrorCode.Unknown,
|
||||
content: [{ text: { value: 'Unknown error occurred' } }],
|
||||
} as ThreadMessage;
|
||||
|
||||
render(<ErrorMessage message={message} />);
|
||||
|
||||
expect(screen.getByText("Apologies, something’s amiss!")).toBeInTheDocument();
|
||||
expect(screen.getByText('troubleshooting assistance')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls regenerateMessage when Regenerate button is clicked', () => {
|
||||
const message: ThreadMessage = {
|
||||
id: '1',
|
||||
status: MessageStatus.Stopped,
|
||||
content: [{ text: { value: 'Test message' } }],
|
||||
} as ThreadMessage;
|
||||
|
||||
render(<ErrorMessage message={message} />);
|
||||
|
||||
fireEvent.click(screen.getByText('Regenerate'));
|
||||
expect(mockResendChatMessage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens troubleshooting modal when link is clicked', () => {
|
||||
const message: ThreadMessage = {
|
||||
id: '1',
|
||||
status: MessageStatus.Error,
|
||||
error_code: ErrorCode.Unknown,
|
||||
content: [{ text: { value: 'Unknown error occurred' } }],
|
||||
} as ThreadMessage;
|
||||
|
||||
render(<ErrorMessage message={message} />);
|
||||
|
||||
fireEvent.click(screen.getByText('troubleshooting assistance'));
|
||||
expect(mockSetModalTroubleShooting).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
69
web/containers/ListContainer/index.test.tsx
Normal file
69
web/containers/ListContainer/index.test.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
// ListContainer.test.tsx
|
||||
import React from 'react'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import ListContainer from './index'
|
||||
|
||||
class ResizeObserverMock {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
global.ResizeObserver = ResizeObserverMock
|
||||
|
||||
describe('ListContainer', () => {
|
||||
const scrollToMock = jest.fn()
|
||||
Element.prototype.scrollTo = scrollToMock
|
||||
|
||||
it('renders children correctly', () => {
|
||||
render(
|
||||
<ListContainer>
|
||||
<div data-testid="child">Test Child</div>
|
||||
</ListContainer>
|
||||
)
|
||||
expect(screen.getByTestId('child')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('scrolls to bottom on initial render', () => {
|
||||
|
||||
render(
|
||||
<ListContainer>
|
||||
<div style={{ height: '1000px' }}>Long content</div>
|
||||
</ListContainer>
|
||||
)
|
||||
|
||||
expect(scrollToMock).toHaveBeenCalledWith({
|
||||
top: expect.any(Number),
|
||||
behavior: 'instant',
|
||||
})
|
||||
})
|
||||
|
||||
it('sets isUserManuallyScrollingUp when scrolling up', () => {
|
||||
const { container } = render(
|
||||
<ListContainer>
|
||||
<div style={{ height: '1000px' }}>Long content</div>
|
||||
</ListContainer>
|
||||
)
|
||||
|
||||
const scrollArea = container.firstChild as HTMLElement
|
||||
|
||||
// Simulate scrolling down
|
||||
fireEvent.scroll(scrollArea, { target: { scrollTop: 500 } })
|
||||
|
||||
// Simulate scrolling up
|
||||
fireEvent.scroll(scrollArea, { target: { scrollTop: 300 } })
|
||||
|
||||
// We can't directly test the internal state, but we can check that
|
||||
// subsequent scroll to bottom doesn't happen (as it would if isUserManuallyScrollingUp was false)
|
||||
|
||||
// Trigger a re-render
|
||||
render(
|
||||
<ListContainer>
|
||||
<div style={{ height: '1000px' }}>Long content</div>
|
||||
</ListContainer>
|
||||
)
|
||||
|
||||
expect(scrollToMock).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -29,7 +29,7 @@ const ListContainer = ({ children }: Props) => {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isUserManuallyScrollingUp.current === true) return
|
||||
if (isUserManuallyScrollingUp.current === true || !listRef.current) return
|
||||
const scrollHeight = listRef.current?.scrollHeight ?? 0
|
||||
listRef.current?.scrollTo({
|
||||
top: scrollHeight,
|
||||
|
||||
75
web/containers/Loader/GenerateResponse.test.tsx
Normal file
75
web/containers/Loader/GenerateResponse.test.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
// GenerateResponse.test.tsx
|
||||
import React from 'react';
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import GenerateResponse from './GenerateResponse';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe('GenerateResponse Component', () => {
|
||||
it('renders initially with 1% loader width', () => {
|
||||
render(<GenerateResponse />);
|
||||
const loader = screen.getByTestId('response-loader');
|
||||
expect(loader).toHaveStyle('width: 24%');
|
||||
});
|
||||
|
||||
it('updates loader width over time', () => {
|
||||
render(<GenerateResponse />);
|
||||
const loader = screen.getByTestId('response-loader');
|
||||
|
||||
// Advance timers to simulate time passing
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
expect(loader).not.toHaveStyle('width: 1%');
|
||||
expect(parseFloat(loader.style.width)).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('pauses at specific percentages', () => {
|
||||
render(<GenerateResponse />);
|
||||
const loader = screen.getByTestId('response-loader');
|
||||
|
||||
// Advance to 24%
|
||||
act(() => {
|
||||
for (let i = 0; i < 24; i++) {
|
||||
jest.advanceTimersByTime(50);
|
||||
}
|
||||
});
|
||||
|
||||
expect(loader).toHaveStyle('width: 50%');
|
||||
|
||||
// Advance past the pause
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(300);
|
||||
});
|
||||
|
||||
expect(loader).toHaveStyle('width: 78%');
|
||||
});
|
||||
|
||||
it('stops at 85%', () => {
|
||||
render(<GenerateResponse />);
|
||||
const loader = screen.getByTestId('response-loader');
|
||||
|
||||
// Advance to 50%
|
||||
act(() => {
|
||||
for (let i = 0; i < 85; i++) {
|
||||
jest.advanceTimersByTime(50);
|
||||
}
|
||||
});
|
||||
|
||||
expect(loader).toHaveStyle('width: 50%');
|
||||
|
||||
// Check if it stays at 78%
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
expect(loader).toHaveStyle('width: 78%');
|
||||
});
|
||||
|
||||
it('displays the correct text', () => {
|
||||
render(<GenerateResponse />);
|
||||
expect(screen.getByText('Generating response...')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
124
web/containers/Loader/ModelReload.test.tsx
Normal file
124
web/containers/Loader/ModelReload.test.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
// ModelReload.test.tsx
|
||||
import React from 'react'
|
||||
import '@testing-library/jest-dom'
|
||||
import { render, screen, act } from '@testing-library/react'
|
||||
import ModelReload from './ModelReload'
|
||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||
|
||||
jest.mock('@/hooks/useActiveModel')
|
||||
|
||||
describe('ModelReload Component', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('renders nothing when not loading', () => {
|
||||
;(useActiveModel as jest.Mock).mockReturnValue({
|
||||
stateModel: { loading: false },
|
||||
})
|
||||
|
||||
const { container } = render(<ModelReload />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('renders loading message when loading', () => {
|
||||
;(useActiveModel as jest.Mock).mockReturnValue({
|
||||
stateModel: { loading: true, model: { id: 'test-model' } },
|
||||
})
|
||||
|
||||
render(<ModelReload />)
|
||||
expect(screen.getByText(/Reloading model test-model/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('updates loader percentage over time', () => {
|
||||
;(useActiveModel as jest.Mock).mockReturnValue({
|
||||
stateModel: { loading: true, model: { id: 'test-model' } },
|
||||
})
|
||||
|
||||
render(<ModelReload />)
|
||||
|
||||
// Initial render
|
||||
expect(screen.getByText(/Reloading model test-model/)).toBeInTheDocument()
|
||||
const loaderElement = screen.getByText(
|
||||
/Reloading model test-model/
|
||||
).parentElement
|
||||
|
||||
// Check initial width
|
||||
expect(loaderElement?.firstChild).toHaveStyle('width: 50%')
|
||||
|
||||
// Advance timers and check updated width
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(250)
|
||||
})
|
||||
expect(loaderElement?.firstChild).toHaveStyle('width: 78%')
|
||||
|
||||
// Advance to 99%
|
||||
for (let i = 0; i < 27; i++) {
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(250)
|
||||
})
|
||||
}
|
||||
expect(loaderElement?.firstChild).toHaveStyle('width: 99%')
|
||||
|
||||
// Advance one more time to hit the 250ms delay
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(250)
|
||||
})
|
||||
expect(loaderElement?.firstChild).toHaveStyle('width: 99%')
|
||||
})
|
||||
|
||||
it('stops at 99%', () => {
|
||||
;(useActiveModel as jest.Mock).mockReturnValue({
|
||||
stateModel: { loading: true, model: { id: 'test-model' } },
|
||||
})
|
||||
|
||||
render(<ModelReload />)
|
||||
|
||||
const loaderElement = screen.getByText(
|
||||
/Reloading model test-model/
|
||||
).parentElement
|
||||
|
||||
// Advance to 99%
|
||||
for (let i = 0; i < 50; i++) {
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(250)
|
||||
})
|
||||
}
|
||||
expect(loaderElement?.firstChild).toHaveStyle('width: 99%')
|
||||
|
||||
// Advance more and check it stays at 99%
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000)
|
||||
})
|
||||
expect(loaderElement?.firstChild).toHaveStyle('width: 99%')
|
||||
})
|
||||
|
||||
it('resets to 0% when loading completes', () => {
|
||||
const { rerender } = render(<ModelReload />)
|
||||
;(useActiveModel as jest.Mock).mockReturnValue({
|
||||
stateModel: { loading: true, model: { id: 'test-model' } },
|
||||
})
|
||||
|
||||
rerender(<ModelReload />)
|
||||
|
||||
const loaderElement = screen.getByText(
|
||||
/Reloading model test-model/
|
||||
).parentElement
|
||||
|
||||
expect(loaderElement?.firstChild).toHaveStyle('width: 50%')
|
||||
// Set loading to false
|
||||
;(useActiveModel as jest.Mock).mockReturnValue({
|
||||
stateModel: { loading: false },
|
||||
})
|
||||
|
||||
rerender(<ModelReload />)
|
||||
|
||||
expect(
|
||||
screen.queryByText(/Reloading model test-model/)
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
22
web/containers/Loader/ProgressCircle.test.tsx
Normal file
22
web/containers/Loader/ProgressCircle.test.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
// ProgressCircle.test.tsx
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import ProgressCircle from './ProgressCircle'
|
||||
|
||||
describe('ProgressCircle Component', () => {
|
||||
test('renders ProgressCircle with default props', () => {
|
||||
render(<ProgressCircle percentage={50} />)
|
||||
const svg = screen.getByRole('img', { hidden: true })
|
||||
expect(svg).toBeInTheDocument()
|
||||
expect(svg).toHaveAttribute('width', '100')
|
||||
expect(svg).toHaveAttribute('height', '100')
|
||||
})
|
||||
|
||||
test('renders ProgressCircle with custom size', () => {
|
||||
render(<ProgressCircle percentage={75} size={200} />)
|
||||
const svg = screen.getByRole('img', { hidden: true })
|
||||
expect(svg).toHaveAttribute('width', '200')
|
||||
expect(svg).toHaveAttribute('height', '200')
|
||||
})
|
||||
})
|
||||
@ -22,6 +22,7 @@ const ProgressCircle: React.FC<ProgressCircleProps> = ({
|
||||
width={size}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
role="img"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
|
||||
105
web/containers/ModalTroubleShoot/AppLogs.test.tsx
Normal file
105
web/containers/ModalTroubleShoot/AppLogs.test.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
// AppLogs.test.tsx
|
||||
import '@testing-library/jest-dom'
|
||||
import React from 'react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import AppLogs from './AppLogs'
|
||||
import { useLogs } from '@/hooks/useLogs'
|
||||
import { usePath } from '@/hooks/usePath'
|
||||
import { useClipboard } from '@/hooks/useClipboard'
|
||||
|
||||
// Mock the hooks
|
||||
jest.mock('@/hooks/useLogs')
|
||||
jest.mock('@/hooks/usePath')
|
||||
jest.mock('@/hooks/useClipboard')
|
||||
|
||||
describe('AppLogs Component', () => {
|
||||
const mockLogs = ['Log 1', 'Log 2', 'Log 3']
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
jest.resetAllMocks()
|
||||
|
||||
// Setup default mock implementations
|
||||
;(useLogs as jest.Mock).mockReturnValue({
|
||||
getLogs: jest.fn().mockResolvedValue(mockLogs.join('\n')),
|
||||
})
|
||||
;(usePath as jest.Mock).mockReturnValue({
|
||||
onRevealInFinder: jest.fn(),
|
||||
})
|
||||
;(useClipboard as jest.Mock).mockReturnValue({
|
||||
copy: jest.fn(),
|
||||
copied: false,
|
||||
})
|
||||
})
|
||||
|
||||
test('renders AppLogs component with logs', async () => {
|
||||
render(<AppLogs />)
|
||||
|
||||
await waitFor(() => {
|
||||
mockLogs.forEach((log) => {
|
||||
expect(screen.getByText(log)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
expect(screen.getByText('Open')).toBeInTheDocument()
|
||||
expect(screen.getByText('Copy All')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('renders empty state when no logs', async () => {
|
||||
;(useLogs as jest.Mock).mockReturnValue({
|
||||
getLogs: jest.fn().mockResolvedValue(''),
|
||||
})
|
||||
|
||||
render(<AppLogs />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Empty logs')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
test('calls onRevealInFinder when Open button is clicked', async () => {
|
||||
const mockOnRevealInFinder = jest.fn()
|
||||
;(usePath as jest.Mock).mockReturnValue({
|
||||
onRevealInFinder: mockOnRevealInFinder,
|
||||
})
|
||||
|
||||
render(<AppLogs />)
|
||||
|
||||
await waitFor(() => {
|
||||
const openButton = screen.getByText('Open')
|
||||
userEvent.click(openButton)
|
||||
|
||||
expect(mockOnRevealInFinder).toHaveBeenCalledWith('Logs')
|
||||
})
|
||||
})
|
||||
|
||||
test('calls copy function when Copy All button is clicked', async () => {
|
||||
const mockCopy = jest.fn()
|
||||
;(useClipboard as jest.Mock).mockReturnValue({
|
||||
copy: mockCopy,
|
||||
copied: false,
|
||||
})
|
||||
|
||||
render(<AppLogs />)
|
||||
|
||||
await waitFor(() => {
|
||||
const copyButton = screen.getByText('Copy All')
|
||||
userEvent.click(copyButton)
|
||||
expect(mockCopy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
test('shows Copying... text when copied is true', async () => {
|
||||
;(useClipboard as jest.Mock).mockReturnValue({
|
||||
copy: jest.fn(),
|
||||
copied: true,
|
||||
})
|
||||
|
||||
render(<AppLogs />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Copying...')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,137 @@
|
||||
// ./AssistantSetting.test.tsx
|
||||
import '@testing-library/jest-dom'
|
||||
import React from 'react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useActiveModel } from '@/hooks/useActiveModel'
|
||||
import { useCreateNewThread } from '@/hooks/useCreateNewThread'
|
||||
import AssistantSetting from './index'
|
||||
|
||||
jest.mock('jotai', () => {
|
||||
const originalModule = jest.requireActual('jotai')
|
||||
return {
|
||||
...originalModule,
|
||||
useAtomValue: jest.fn(),
|
||||
useSetAtom: jest.fn(),
|
||||
}
|
||||
})
|
||||
jest.mock('@/hooks/useActiveModel')
|
||||
jest.mock('@/hooks/useCreateNewThread')
|
||||
jest.mock('./../../../../containers/ModelSetting/SettingComponent', () => {
|
||||
return jest.fn().mockImplementation(({ onValueUpdated }) => {
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
data-testid="input"
|
||||
onChange={(e) => onValueUpdated('chunk_size', e.target.value)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('AssistantSetting Component', () => {
|
||||
const mockActiveThread = {
|
||||
id: '123',
|
||||
assistants: [
|
||||
{
|
||||
id: '456',
|
||||
tools: [
|
||||
{
|
||||
type: 'retrieval',
|
||||
enabled: true,
|
||||
settings: {
|
||||
chunk_size: 100,
|
||||
chunk_overlap: 50,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
const ComponentPropsMock: any[] = [
|
||||
{
|
||||
key: 'chunk_size',
|
||||
type: 'number',
|
||||
title: 'Chunk Size',
|
||||
value: 100,
|
||||
controllerType: 'input',
|
||||
},
|
||||
{
|
||||
key: 'chunk_overlap',
|
||||
type: 'number',
|
||||
title: 'Chunk Overlap',
|
||||
value: 50,
|
||||
controllerType: 'input',
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
test('renders AssistantSetting component with proper data', async () => {
|
||||
const setEngineParamsUpdate = jest.fn()
|
||||
;(useSetAtom as jest.Mock).mockImplementationOnce(
|
||||
() => setEngineParamsUpdate
|
||||
)
|
||||
;(useAtomValue as jest.Mock).mockImplementationOnce(() => mockActiveThread)
|
||||
const updateThreadMetadata = jest.fn()
|
||||
;(useActiveModel as jest.Mock).mockReturnValueOnce({ stopModel: jest.fn() })
|
||||
;(useCreateNewThread as jest.Mock).mockReturnValueOnce({
|
||||
updateThreadMetadata,
|
||||
})
|
||||
|
||||
render(<AssistantSetting componentData={ComponentPropsMock} />)
|
||||
|
||||
await waitFor(() => {
|
||||
const firstInput = screen.getByTestId('input')
|
||||
expect(firstInput).toBeInTheDocument()
|
||||
|
||||
userEvent.type(firstInput, '200')
|
||||
expect(updateThreadMetadata).toHaveBeenCalled()
|
||||
expect(setEngineParamsUpdate).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
|
||||
test('triggers model reload with onValueChanged', async () => {
|
||||
const setEngineParamsUpdate = jest.fn()
|
||||
const updateThreadMetadata = jest.fn()
|
||||
const stopModel = jest.fn()
|
||||
;(useAtomValue as jest.Mock).mockImplementationOnce(() => mockActiveThread)
|
||||
;(useSetAtom as jest.Mock).mockImplementation(() => setEngineParamsUpdate)
|
||||
;(useActiveModel as jest.Mock).mockReturnValueOnce({ stopModel })
|
||||
;(useCreateNewThread as jest.Mock).mockReturnValueOnce({
|
||||
updateThreadMetadata,
|
||||
})
|
||||
;(useCreateNewThread as jest.Mock).mockReturnValueOnce({
|
||||
updateThreadMetadata,
|
||||
})
|
||||
|
||||
render(
|
||||
<AssistantSetting
|
||||
componentData={
|
||||
[
|
||||
{
|
||||
key: 'chunk_size',
|
||||
type: 'number',
|
||||
title: 'Chunk Size',
|
||||
value: 100,
|
||||
controllerType: 'input',
|
||||
requireModelReload: true,
|
||||
},
|
||||
] as any
|
||||
}
|
||||
/>
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
const firstInput = screen.getByTestId('input')
|
||||
expect(firstInput).toBeInTheDocument()
|
||||
|
||||
userEvent.type(firstInput, '200')
|
||||
expect(setEngineParamsUpdate).toHaveBeenCalled()
|
||||
expect(stopModel).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user