Skip to content

Commit eec0b64

Browse files
authored
Merge pull request #86 from nemozak1/opeyII_integration
WIP Tests and Opey Mocking for the OpeyController
2 parents 6aca5b8 + f69070e commit eec0b64

5 files changed

Lines changed: 367 additions & 83 deletions

File tree

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,11 @@
6464
"@babel/preset-env": "^7.26.8",
6565
"@babel/preset-typescript": "^7.26.0",
6666
"@rushstack/eslint-patch": "^1.4.0",
67+
"@types/express": "^5.0.0",
6768
"@types/jsdom": "^21.1.7",
6869
"@types/jsonwebtoken": "^9.0.6",
6970
"@types/markdown-it": "^14.1.1",
70-
"@types/node": "^20.17.17",
71+
"@types/node": "^22.13.4",
7172
"@vitejs/plugin-vue": "^4.3.0",
7273
"@vitejs/plugin-vue-jsx": "^3.1.0",
7374
"@vue/eslint-config-prettier": "^9.0.0",
@@ -79,6 +80,7 @@
7980
"eslint-plugin-vue": "^9.12.0",
8081
"jest": "^29.7.0",
8182
"jsdom": "^25.0.1",
83+
"node-mocks-http": "^1.16.2",
8284
"npm-run-all2": "^7.0.1",
8385
"prettier": "^3.0.1",
8486
"superagent": "^9.0.0",

server/controllers/OpeyIIController.ts

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ import { UserInput } from '../schema/OpeySchema'
1414

1515
export class OpeyController {
1616
constructor(
17-
private obpClientService: OBPClientService,
18-
private opeyClientService: OpeyClientService,
17+
public obpClientService: OBPClientService,
18+
public opeyClientService: OpeyClientService,
1919
) {}
2020

2121
@Get('/')
2222
async getStatus(
2323
@Res() response: Response
24-
): Response {
24+
): Promise<Response | any> {
2525

2626
try {
2727
const opeyStatus = await this.opeyClientService.getOpeyStatus()
@@ -67,33 +67,57 @@ export class OpeyController {
6767
callback();
6868
}
6969
})
70+
71+
let nodeStream: NodeJS.ReadableStream | null = null
7072

7173
try {
72-
const nodeStream = await this.opeyClientService.stream(user_input)
73-
console.log(`Stream received from OpeyClientService.stream: ${nodeStream.readable}`)
74-
nodeStream.pipe(streamMiddlewareTransform).pipe(response)
74+
// Read stream from OpeyClientService
75+
nodeStream = await this.opeyClientService.stream(user_input)
76+
console.debug(`Stream received readable: ${nodeStream.readable}`)
77+
78+
} catch (error) {
79+
console.error("Error reading stream: ", error)
80+
response.status(500).json({ error: 'Internal Server Error' })
81+
return
82+
}
83+
84+
if (!nodeStream || !nodeStream.readable) {
85+
console.error("Stream is not readable")
86+
response.status(500).json({ error: 'Internal Server Error' })
87+
return
88+
}
89+
90+
try {
91+
// response.writeHead(200, {
92+
// 'Content-Type': "text/event-stream",
93+
// 'Cache-Control': "no-cache",
94+
// 'Connection': "keep-alive"
95+
// });
7596

76-
response.status(200)
7797
response.setHeader('Content-Type', 'text/event-stream')
7898
response.setHeader('Cache-Control', 'no-cache')
7999
response.setHeader('Connection', 'keep-alive')
80100

81-
// nodeStream.on('data', (chunk) => {
82-
// const data = chunk.toString()
83-
// console.log(`data: ${data}`)
84-
// response.write(`data: ${data}\n\n`)
85-
// })
86-
// nodeStream.on('end', () => {
87-
// console.log('Stream ended')
88-
// response.end()
89-
// })
90-
// nodeStream.on('error', (error) => {
91-
// console.error(error)
92-
// response.write(`data: Error reading stream\n\n`)
93-
// response.end()
94-
// })
101+
let data: any[] = []
102+
103+
nodeStream.on('data', (chunk) => {
104+
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
105+
data.push(bufferChunk);
106+
response.write(`data: ${chunk.toString()}\n\n`)
107+
})
108+
nodeStream.on('end', () => {
109+
//console.log('Stream ended')
110+
const totalData = Buffer.concat(data)
111+
response.write(totalData)
112+
response.end()
113+
})
114+
nodeStream.on('error', (error) => {
115+
console.error(error)
116+
response.write(`data: Error reading stream\n\n`)
117+
response.end()
118+
})
95119
} catch (error) {
96-
console.error(error)
120+
console.error("Error writing data: ", error)
97121
response.status(500).json({ error: 'Internal Server Error' })
98122
}
99123
}
@@ -103,7 +127,7 @@ export class OpeyController {
103127
@Session() session: any,
104128
@Req() request: Request,
105129
@Res() response: Response
106-
): Response {
130+
): Promise<Response | any> {
107131

108132
let user_input: UserInput
109133
try {
@@ -113,14 +137,14 @@ export class OpeyController {
113137
"is_tool_call_approval": request.body.is_tool_call_approval
114138
}
115139
} catch (error) {
116-
console.error("Error in stream endpoint, could not parse into UserInput: ", error)
140+
console.error("Error in invoke endpoint, could not parse into UserInput: ", error)
117141
return response.status(500).json({ error: 'Internal Server Error' })
118142
}
119143

120144
try {
121145
const opey_response = await this.opeyClientService.invoke(user_input)
122146

123-
console.log("Opey response: ", opey_response)
147+
//console.log("Opey response: ", opey_response)
124148
return response.status(200).json(opey_response)
125149
} catch (error) {
126150
console.error(error)
@@ -136,7 +160,7 @@ export class OpeyController {
136160
@Session() session: any,
137161
@Req() request: Request,
138162
@Res() response: Response
139-
): Response {
163+
): Promise<Response | any> {
140164
try {
141165
console.log("Getting consent from OBP")
142166
// Check if consent is already in session
@@ -190,7 +214,7 @@ export class OpeyController {
190214
@Session() session: any,
191215
@Req() request: Request,
192216
@Res() response: Response
193-
): Response {
217+
): Promise<Response | any> {
194218
try {
195219
const oauthConfig = session['clientConfig']
196220
const version = this.obpClientService.getOBPVersion()

tests/opey-unit.test.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { OpeyController } from "../server/controllers/OpeyIIController";
2+
import OpeyClientService from '../server/services/OpeyClientService';
3+
import OBPClientService from '../server/services/OBPClientService';
4+
import Stream, { Readable } from 'stream';
5+
import { Request, Response } from 'express';
6+
import httpMocks from 'node-mocks-http'
7+
import { EventEmitter } from 'events';
8+
import {jest} from '@jest/globals';
9+
10+
jest.mock("../server/services/OpeyClientService", () => {
11+
return {
12+
OpeyClientService: jest.fn().mockImplementation(() => {
13+
return {
14+
getOpeyStatus: jest.fn(async () => {
15+
return {status: 'running'}
16+
}),
17+
stream: jest.fn(async () => {
18+
const readableStream = new Stream.Readable();
19+
20+
for (let i=0; i<10; i++) {
21+
readableStream.push(`Chunk ${i}`);
22+
}
23+
24+
return readableStream as NodeJS.ReadableStream;
25+
}),
26+
invoke: jest.fn(async () => {
27+
return {
28+
content: 'Hi this is Opey',
29+
}
30+
})
31+
}
32+
33+
}),
34+
};
35+
});
36+
37+
// jest.mock("./A", () => {
38+
// return {
39+
// A: jest.fn().mockImplementation(() => {
40+
// return {
41+
// getSomething: getSomethingMock
42+
// }
43+
// })
44+
// };
45+
// });
46+
// Mock the OpeyClientService class
47+
48+
49+
// jest.mocked(OpeyClientService).mockImplementation(() => {
50+
// return {
51+
// getOpeyStatus: jest.fn(async () => {
52+
// return {status: 'running'}
53+
// }),
54+
// stream: jest.fn(async () => {
55+
// const readableStream = new Stream.Readable();
56+
57+
// for (let i=0; i<10; i++) {
58+
// readableStream.push(`Chunk ${i}`);
59+
// }
60+
61+
// return readableStream as NodeJS.ReadableStream;
62+
// }),
63+
// invoke: jest.fn(async () => {
64+
// return {
65+
// content: 'Hi this is Opey',
66+
// }
67+
// })
68+
// }
69+
// });
70+
71+
72+
73+
describe('OpeyController', () => {
74+
// Mock the OpeyClientService class
75+
76+
const MockOpeyClientService = {
77+
authConfig: {},
78+
opeyConfig: {},
79+
getOpeyStatus: jest.fn(async () => {
80+
return {status: 'running'}
81+
}),
82+
stream: jest.fn(async () => {
83+
84+
async function * generator() {
85+
for (let i=0; i<10; i++) {
86+
yield `Chunk ${i}`;
87+
}
88+
}
89+
90+
const readableStream = Stream.Readable.from(generator());
91+
92+
return readableStream as NodeJS.ReadableStream;
93+
}),
94+
invoke: jest.fn(async () => {
95+
return {
96+
content: 'Hi this is Opey',
97+
}
98+
})
99+
} as unknown as jest.Mocked<OpeyClientService>
100+
101+
102+
// Instantiate OpeyController with the mocked OpeyClientService
103+
const opeyController = new OpeyController(new OBPClientService, MockOpeyClientService)
104+
105+
106+
it('getStatus', async () => {
107+
const res = httpMocks.createResponse();
108+
109+
await opeyController.getStatus(res)
110+
expect(MockOpeyClientService.getOpeyStatus).toHaveBeenCalled();
111+
expect(res.statusCode).toBe(200);
112+
})
113+
114+
it('streamOpey', () => {
115+
116+
const _eventEmitter = new EventEmitter();
117+
_eventEmitter.addListener('data', () => {
118+
console.log('Data received')
119+
})
120+
// The default event emitter does nothing, so replace
121+
const res = httpMocks.createResponse({
122+
eventEmitter: _eventEmitter,
123+
writableStream: Stream.Writable
124+
});
125+
126+
const req = {
127+
body: {
128+
message: 'Hello Opey',
129+
thread_id: '123',
130+
is_tool_call_approval: false
131+
}
132+
} as unknown as Request;
133+
134+
// Define handelrs for events
135+
res.on('end', () => {
136+
console.log('Stream ended')
137+
console.log(res._getData())
138+
expect(res.statusCode).toBe(200);
139+
})
140+
141+
let chunks: any[] = [];
142+
res.on('data', (chunk) => {
143+
console.log(chunk)
144+
chunks.push(chunk);
145+
expect(chunk).toBeDefined();
146+
})
147+
148+
opeyController.streamOpey({}, req, res)
149+
.then((res) => {
150+
console.log(res)
151+
})
152+
153+
expect(chunks.length).toBe(10);
154+
expect(MockOpeyClientService.stream).toHaveBeenCalled();
155+
expect(res).toBeDefined();
156+
157+
})
158+
})

0 commit comments

Comments
 (0)