From 3321235c7fac9b233fc1fdfb0c70bb47a8ad3c1b Mon Sep 17 00:00:00 2001 From: nemo Date: Fri, 14 Feb 2025 17:27:55 +0000 Subject: [PATCH 1/2] tests WIP --- package.json | 4 +- server/controllers/OpeyIIController.ts | 78 ++++++++----- tests/opey-unit.test.ts | 154 +++++++++++++++++++++++++ tests/opey.test.ts | 83 +++++++------ yarn.lock | 127 +++++++++++++++++--- 5 files changed, 363 insertions(+), 83 deletions(-) create mode 100644 tests/opey-unit.test.ts diff --git a/package.json b/package.json index c81a2f4..d4fbc70 100644 --- a/package.json +++ b/package.json @@ -64,10 +64,11 @@ "@babel/preset-env": "^7.26.8", "@babel/preset-typescript": "^7.26.0", "@rushstack/eslint-patch": "^1.4.0", + "@types/express": "^5.0.0", "@types/jsdom": "^21.1.7", "@types/jsonwebtoken": "^9.0.6", "@types/markdown-it": "^14.1.1", - "@types/node": "^20.17.17", + "@types/node": "^22.13.4", "@vitejs/plugin-vue": "^4.3.0", "@vitejs/plugin-vue-jsx": "^3.1.0", "@vue/eslint-config-prettier": "^9.0.0", @@ -79,6 +80,7 @@ "eslint-plugin-vue": "^9.12.0", "jest": "^29.7.0", "jsdom": "^25.0.1", + "node-mocks-http": "^1.16.2", "npm-run-all2": "^7.0.1", "prettier": "^3.0.1", "superagent": "^9.0.0", diff --git a/server/controllers/OpeyIIController.ts b/server/controllers/OpeyIIController.ts index 1fb890e..bed47eb 100644 --- a/server/controllers/OpeyIIController.ts +++ b/server/controllers/OpeyIIController.ts @@ -14,14 +14,14 @@ import { UserInput } from '../schema/OpeySchema' export class OpeyController { constructor( - private obpClientService: OBPClientService, - private opeyClientService: OpeyClientService, + public obpClientService: OBPClientService, + public opeyClientService: OpeyClientService, ) {} @Get('/') async getStatus( @Res() response: Response - ): Response { + ): Promise { try { const opeyStatus = await this.opeyClientService.getOpeyStatus() @@ -67,33 +67,57 @@ export class OpeyController { callback(); } }) + + let nodeStream: NodeJS.ReadableStream | null = null try { - const nodeStream = await this.opeyClientService.stream(user_input) - console.log(`Stream received from OpeyClientService.stream: ${nodeStream.readable}`) - nodeStream.pipe(streamMiddlewareTransform).pipe(response) + // Read stream from OpeyClientService + nodeStream = await this.opeyClientService.stream(user_input) + console.debug(`Stream received readable: ${nodeStream.readable}`) + + } catch (error) { + console.error("Error reading stream: ", error) + response.status(500).json({ error: 'Internal Server Error' }) + return + } + + if (!nodeStream || !nodeStream.readable) { + console.error("Stream is not readable") + response.status(500).json({ error: 'Internal Server Error' }) + return + } + + try { + // response.writeHead(200, { + // 'Content-Type': "text/event-stream", + // 'Cache-Control': "no-cache", + // 'Connection': "keep-alive" + // }); - response.status(200) response.setHeader('Content-Type', 'text/event-stream') response.setHeader('Cache-Control', 'no-cache') response.setHeader('Connection', 'keep-alive') - // nodeStream.on('data', (chunk) => { - // const data = chunk.toString() - // console.log(`data: ${data}`) - // response.write(`data: ${data}\n\n`) - // }) - // nodeStream.on('end', () => { - // console.log('Stream ended') - // response.end() - // }) - // nodeStream.on('error', (error) => { - // console.error(error) - // response.write(`data: Error reading stream\n\n`) - // response.end() - // }) + let data: any[] = [] + + nodeStream.on('data', (chunk) => { + const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + data.push(bufferChunk); + response.write(`data: ${chunk.toString()}\n\n`) + }) + nodeStream.on('end', () => { + //console.log('Stream ended') + const totalData = Buffer.concat(data) + response.write(totalData) + response.end() + }) + nodeStream.on('error', (error) => { + console.error(error) + response.write(`data: Error reading stream\n\n`) + response.end() + }) } catch (error) { - console.error(error) + console.error("Error writing data: ", error) response.status(500).json({ error: 'Internal Server Error' }) } } @@ -103,7 +127,7 @@ export class OpeyController { @Session() session: any, @Req() request: Request, @Res() response: Response - ): Response { + ): Promise { let user_input: UserInput try { @@ -113,14 +137,14 @@ export class OpeyController { "is_tool_call_approval": request.body.is_tool_call_approval } } catch (error) { - console.error("Error in stream endpoint, could not parse into UserInput: ", error) + console.error("Error in invoke endpoint, could not parse into UserInput: ", error) return response.status(500).json({ error: 'Internal Server Error' }) } try { const opey_response = await this.opeyClientService.invoke(user_input) - console.log("Opey response: ", opey_response) + //console.log("Opey response: ", opey_response) return response.status(200).json(opey_response) } catch (error) { console.error(error) @@ -136,7 +160,7 @@ export class OpeyController { @Session() session: any, @Req() request: Request, @Res() response: Response - ): Response { + ): Promise { try { console.log("Getting consent from OBP") // Check if consent is already in session @@ -190,7 +214,7 @@ export class OpeyController { @Session() session: any, @Req() request: Request, @Res() response: Response - ): Response { + ): Promise { try { const oauthConfig = session['clientConfig'] const version = this.obpClientService.getOBPVersion() diff --git a/tests/opey-unit.test.ts b/tests/opey-unit.test.ts new file mode 100644 index 0000000..13c2903 --- /dev/null +++ b/tests/opey-unit.test.ts @@ -0,0 +1,154 @@ +import { OpeyController } from "../server/controllers/OpeyIIController"; +import OpeyClientService from '../server/services/OpeyClientService'; +import OBPClientService from '../server/services/OBPClientService'; +import Stream, { Readable } from 'stream'; +import { Request, Response } from 'express'; +import httpMocks from 'node-mocks-http' +import { EventEmitter } from 'events'; +import {jest} from '@jest/globals'; + +jest.mock("../server/services/OpeyClientService", () => { + return { + OpeyClientService: jest.fn().mockImplementation(() => { + return { + getOpeyStatus: jest.fn(async () => { + return {status: 'running'} + }), + stream: jest.fn(async () => { + const readableStream = new Stream.Readable(); + + for (let i=0; i<10; i++) { + readableStream.push(`Chunk ${i}`); + } + + return readableStream as NodeJS.ReadableStream; + }), + invoke: jest.fn(async () => { + return { + content: 'Hi this is Opey', + } + }) + } + + }), + }; +}); + +// jest.mock("./A", () => { +// return { +// A: jest.fn().mockImplementation(() => { +// return { +// getSomething: getSomethingMock +// } +// }) +// }; +// }); +// Mock the OpeyClientService class + + +// jest.mocked(OpeyClientService).mockImplementation(() => { +// return { +// getOpeyStatus: jest.fn(async () => { +// return {status: 'running'} +// }), +// stream: jest.fn(async () => { +// const readableStream = new Stream.Readable(); + +// for (let i=0; i<10; i++) { +// readableStream.push(`Chunk ${i}`); +// } + +// return readableStream as NodeJS.ReadableStream; +// }), +// invoke: jest.fn(async () => { +// return { +// content: 'Hi this is Opey', +// } +// }) +// } +// }); + + + +describe('OpeyController', () => { + // Mock the OpeyClientService class + + const MockOpeyClientService = { + authConfig: {}, + opeyConfig: {}, + getOpeyStatus: jest.fn(async () => { + return {status: 'running'} + }), + stream: jest.fn(async () => { + + async function * generator() { + for (let i=0; i<10; i++) { + yield `Chunk ${i}`; + } + } + + const readableStream = Stream.Readable.from(generator()); + + return readableStream as NodeJS.ReadableStream; + }), + invoke: jest.fn(async () => { + return { + content: 'Hi this is Opey', + } + }) + } as unknown as jest.Mocked + + + // Instantiate OpeyController with the mocked OpeyClientService + const opeyController = new OpeyController(new OBPClientService, MockOpeyClientService) + + + it('getStatus', async () => { + const res = httpMocks.createResponse(); + + await opeyController.getStatus(res) + expect(MockOpeyClientService.getOpeyStatus).toHaveBeenCalled(); + expect(res.statusCode).toBe(200); + }) + + it('streamOpey', () => { + + // The default event emitter does nothing, so replace + const res = httpMocks.createResponse({ + eventEmitter: EventEmitter, + writableStream: Stream.Writable + }); + + const req = { + body: { + message: 'Hello Opey', + thread_id: '123', + is_tool_call_approval: false + } + } as unknown as Request; + + // Define handelrs for events + res.on('end', () => { + console.log('Stream ended') + console.log(res._getData()) + expect(res.statusCode).toBe(200); + }) + + let chunks: any[] = []; + res.on('data', (chunk) => { + console.log(chunk) + chunks.push(chunk); + expect(chunk).toBeDefined(); + }) + + opeyController.streamOpey({}, req, res) + .then((res) => { + console.log(res) + }) + + expect(chunks.length).toBe(10); + expect(MockOpeyClientService.stream).toHaveBeenCalled(); + expect(res).toBeDefined(); + + }) +}) diff --git a/tests/opey.test.ts b/tests/opey.test.ts index 74d9209..d884228 100644 --- a/tests/opey.test.ts +++ b/tests/opey.test.ts @@ -1,5 +1,4 @@ -import { OpeyController } from "../server/controllers/OpeyController"; -import app from '../server/app'; +import app, { instance } from '../server/app'; import request from 'supertest'; import fetch from 'node-fetch'; import http from 'node:http'; @@ -24,49 +23,72 @@ describe('GET /api/opey', () => { }); describe('GET /api/opey/invoke', () => { - let response: Response; + let response; let userInput: UserInput = { message: "Hello Opey", thread_id: uuidv4(), is_tool_call_approval: false } - - it('Should return 200', async () => { - const response = await request(app) + + beforeAll(async () => { + // Make the invoke request + response = await request(app) .post("/api/opey/invoke") .send(userInput) .set('Content-Type', 'application/json') - .then(response => { - console.log(`Response: ${response.body}`) - expect(response.status).toBe(200); - }) - - + }) + + it('Should return 200', async () => { + expect(response.status).toBe(200); }); + + it('Should return a message if not a tool call approval', async () => { + if (!response.body.tool_approval_request) { + expect(response.body.content).toBeTruthy(); + } + }) }) describe('POST /api/opey/stream', () => { + let streamingResponse; + + let userInput: UserInput = { + message: "Hello Opey", + thread_id: uuidv4(), + is_tool_call_approval: false + } + const httpAgent = new http.Agent({ keepAlive: true, port: 9999 }); beforeAll(async () => { app.listen(5173) + try { + streamingResponse = await fetch(`${SERVER_URL}/api/opey/stream`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'connection': 'keep-alive' + }, + body: JSON.stringify(userInput), + }) + } catch (error) { + console.error(`Error getting stream: ${error}`) + } + }); afterAll(async () => { - app.close() + instance.close() httpAgent.destroy() }); + it it('Should stream response', async () => { - let userInput: UserInput = { - message: "Hello Opey", - thread_id: uuidv4(), - is_tool_call_approval: false - } + @@ -76,23 +98,11 @@ describe('POST /api/opey/stream', () => { // .responseType('blob') // .send(userInput) - await fetch(`${SERVER_URL}/api/opey/stream`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'connection': 'keep-alive' - }, - body: JSON.stringify(userInput), - }) - .catch(error => { - console.error(`Error performing test fetch: ${error}`) + expect(streamingResponse.status).toBe(200) + + streamingResponse.body.on('data', (chunk) => { + console.log(`${chunk}`) }) - .then(streamingResponse => { - console.log(streamingResponse) - - streamingResponse.body.on('data', (chunk) => { - console.log(`${chunk}`) - }) // response.on // console.log(response.body) // const readable = response.body @@ -100,10 +110,7 @@ describe('POST /api/opey/stream', () => { // const data = chunk.toString() // console.log(`data: ${data}`) // }) - }) - .finally(() => { - httpAgent.destroy() - }) + diff --git a/yarn.lock b/yarn.lock index 5898022..a48fbdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1797,6 +1797,14 @@ dependencies: "@babel/types" "^7.20.7" +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + "@types/chai-subset@^1.3.3": version "1.3.3" resolved "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz" @@ -1809,6 +1817,13 @@ resolved "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz" integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ== +"@types/connect@*": + version "3.4.38" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + "@types/cookie@^0.4.1": version "0.4.1" resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz" @@ -1836,6 +1851,26 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== +"@types/express-serve-static-core@^5.0.0": + version "5.0.6" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz" + integrity sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.21 || ^5.0.0", "@types/express@^5.0.0": + version "5.0.0" + resolved "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz" + integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^5.0.0" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/gensync@^1.0.0": version "1.0.4" resolved "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.4.tgz" @@ -1853,6 +1888,11 @@ resolved "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz" integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.6" resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz" @@ -1936,12 +1976,44 @@ resolved "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz" integrity sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ== -"@types/node@*", "@types/node@^20.17.17", "@types/node@>= 14", "@types/node@>=10.0.0": - version "20.17.17" - resolved "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz" - integrity sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg== +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node@*", "@types/node@^22.13.4", "@types/node@>= 14", "@types/node@>=10.0.0": + version "22.13.4" + resolved "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz" + integrity sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg== dependencies: - undici-types "~6.19.2" + undici-types "~6.20.0" + +"@types/qs@*": + version "6.9.18" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz" + integrity sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.7" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" "@types/stack-utils@^2.0.0": version "2.0.3" @@ -2349,7 +2421,7 @@ abbrev@^2.0.0: resolved "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz" integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== -accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.8: +accepts@^1.3.5, accepts@^1.3.7, accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -3224,7 +3296,7 @@ constants-browserify@^1.0.0: resolved "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz" integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== -content-disposition@~0.5.2, content-disposition@0.5.4: +content-disposition@^0.5.3, content-disposition@~0.5.2, content-disposition@0.5.4: version "0.5.4" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== @@ -3526,6 +3598,11 @@ delegates@^1.0.0: resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== +depd@^1.1.0: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + depd@^2.0.0, depd@~2.0.0, depd@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" @@ -4370,7 +4447,7 @@ forwarded@0.2.0: resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -fresh@~0.5.2, fresh@0.5.2: +fresh@^0.5.2, fresh@~0.5.2, fresh@0.5.2: version "0.5.2" resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== @@ -5905,7 +5982,7 @@ memorystream@^0.3.1: resolved "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz" integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== -merge-descriptors@1.0.3: +merge-descriptors@^1.0.1, merge-descriptors@1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== @@ -5953,7 +6030,7 @@ mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.24, mime-types@~2.1.34: dependencies: mime-db "1.52.0" -mime@1.6.0: +mime@^1.3.4, mime@1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -6127,6 +6204,22 @@ node-int64@^0.4.0: resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== +node-mocks-http@^1.16.2: + version "1.16.2" + resolved "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.16.2.tgz" + integrity sha512-2Sh6YItRp1oqewZNlck3LaFp5vbyW2u51HX2p1VLxQ9U/bG90XV8JY9O7Nk+HDd6OOn/oV3nA5Tx5k4Rki0qlg== + dependencies: + accepts "^1.3.7" + content-disposition "^0.5.3" + depd "^1.1.0" + fresh "^0.5.2" + merge-descriptors "^1.0.1" + methods "^1.1.2" + mime "^1.3.4" + parseurl "^1.3.3" + range-parser "^1.2.0" + type-is "^1.6.18" + node-releases@^2.0.19: version "2.0.19" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz" @@ -6426,7 +6519,7 @@ parseuri@0.0.6: resolved "https://registry.npmjs.org/parseuri/-/parseuri-0.0.6.tgz" integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== -parseurl@^1.3.2, parseurl@~1.3.3: +parseurl@^1.3.2, parseurl@^1.3.3, parseurl@~1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -6724,7 +6817,7 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" -range-parser@~1.2.1: +range-parser@^1.2.0, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== @@ -7707,7 +7800,7 @@ type-fest@^4.26.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.33.0.tgz" integrity sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g== -type-is@^1.6.16, type-is@^1.6.4, type-is@~1.6.18: +type-is@^1.6.16, type-is@^1.6.18, type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -7756,10 +7849,10 @@ uid-safe@~2.1.5: dependencies: random-bytes "~1.0.0" -undici-types@~6.19.2: - version "6.19.8" - resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~6.20.0: + version "6.20.0" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz" + integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== undici@^6.19.5: version "6.21.0" From fe3de28e958757ea94ab7ff2325dbf6a91ea30de Mon Sep 17 00:00:00 2001 From: nemo Date: Mon, 17 Feb 2025 12:17:10 +0000 Subject: [PATCH 2/2] add unit test for OpeyController --- tests/opey-unit.test.ts | 158 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 tests/opey-unit.test.ts diff --git a/tests/opey-unit.test.ts b/tests/opey-unit.test.ts new file mode 100644 index 0000000..794a88f --- /dev/null +++ b/tests/opey-unit.test.ts @@ -0,0 +1,158 @@ +import { OpeyController } from "../server/controllers/OpeyIIController"; +import OpeyClientService from '../server/services/OpeyClientService'; +import OBPClientService from '../server/services/OBPClientService'; +import Stream, { Readable } from 'stream'; +import { Request, Response } from 'express'; +import httpMocks from 'node-mocks-http' +import { EventEmitter } from 'events'; +import {jest} from '@jest/globals'; + +jest.mock("../server/services/OpeyClientService", () => { + return { + OpeyClientService: jest.fn().mockImplementation(() => { + return { + getOpeyStatus: jest.fn(async () => { + return {status: 'running'} + }), + stream: jest.fn(async () => { + const readableStream = new Stream.Readable(); + + for (let i=0; i<10; i++) { + readableStream.push(`Chunk ${i}`); + } + + return readableStream as NodeJS.ReadableStream; + }), + invoke: jest.fn(async () => { + return { + content: 'Hi this is Opey', + } + }) + } + + }), + }; +}); + +// jest.mock("./A", () => { +// return { +// A: jest.fn().mockImplementation(() => { +// return { +// getSomething: getSomethingMock +// } +// }) +// }; +// }); +// Mock the OpeyClientService class + + +// jest.mocked(OpeyClientService).mockImplementation(() => { +// return { +// getOpeyStatus: jest.fn(async () => { +// return {status: 'running'} +// }), +// stream: jest.fn(async () => { +// const readableStream = new Stream.Readable(); + +// for (let i=0; i<10; i++) { +// readableStream.push(`Chunk ${i}`); +// } + +// return readableStream as NodeJS.ReadableStream; +// }), +// invoke: jest.fn(async () => { +// return { +// content: 'Hi this is Opey', +// } +// }) +// } +// }); + + + +describe('OpeyController', () => { + // Mock the OpeyClientService class + + const MockOpeyClientService = { + authConfig: {}, + opeyConfig: {}, + getOpeyStatus: jest.fn(async () => { + return {status: 'running'} + }), + stream: jest.fn(async () => { + + async function * generator() { + for (let i=0; i<10; i++) { + yield `Chunk ${i}`; + } + } + + const readableStream = Stream.Readable.from(generator()); + + return readableStream as NodeJS.ReadableStream; + }), + invoke: jest.fn(async () => { + return { + content: 'Hi this is Opey', + } + }) + } as unknown as jest.Mocked + + + // Instantiate OpeyController with the mocked OpeyClientService + const opeyController = new OpeyController(new OBPClientService, MockOpeyClientService) + + + it('getStatus', async () => { + const res = httpMocks.createResponse(); + + await opeyController.getStatus(res) + expect(MockOpeyClientService.getOpeyStatus).toHaveBeenCalled(); + expect(res.statusCode).toBe(200); + }) + + it('streamOpey', () => { + + const _eventEmitter = new EventEmitter(); + _eventEmitter.addListener('data', () => { + console.log('Data received') + }) + // The default event emitter does nothing, so replace + const res = httpMocks.createResponse({ + eventEmitter: _eventEmitter, + writableStream: Stream.Writable + }); + + const req = { + body: { + message: 'Hello Opey', + thread_id: '123', + is_tool_call_approval: false + } + } as unknown as Request; + + // Define handelrs for events + res.on('end', () => { + console.log('Stream ended') + console.log(res._getData()) + expect(res.statusCode).toBe(200); + }) + + let chunks: any[] = []; + res.on('data', (chunk) => { + console.log(chunk) + chunks.push(chunk); + expect(chunk).toBeDefined(); + }) + + opeyController.streamOpey({}, req, res) + .then((res) => { + console.log(res) + }) + + expect(chunks.length).toBe(10); + expect(MockOpeyClientService.stream).toHaveBeenCalled(); + expect(res).toBeDefined(); + + }) +})