mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 02:36:46 +00:00
tool messages WIP and add integration test framework
This commit is contained in:
parent
4e689dfaa8
commit
8a660065f7
45
package-lock.json
generated
45
package-lock.json
generated
@ -70,6 +70,7 @@
|
||||
"jsdom": "^25.0.1",
|
||||
"node-mocks-http": "^1.16.2",
|
||||
"npm-run-all2": "^7.0.1",
|
||||
"playwright": "^1.51.1",
|
||||
"prettier": "^3.0.1",
|
||||
"superagent": "^9.0.0",
|
||||
"supertest": "^7.0.0",
|
||||
@ -11249,6 +11250,50 @@
|
||||
"pathe": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.51.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz",
|
||||
"integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright-core": "1.51.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.51.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz",
|
||||
"integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
||||
@ -14,7 +14,10 @@
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/ server/"
|
||||
"format": "prettier --write src/ server/",
|
||||
"test:integration": "vitest run --config vitest.integration.config.js",
|
||||
"test:integration:watch": "vitest --config vitest.integration.config.js",
|
||||
"test:integration:ui": "vitest --ui --config vitest.integration.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
@ -79,6 +82,7 @@
|
||||
"jsdom": "^25.0.1",
|
||||
"node-mocks-http": "^1.16.2",
|
||||
"npm-run-all2": "^7.0.1",
|
||||
"playwright": "^1.51.1",
|
||||
"prettier": "^3.0.1",
|
||||
"superagent": "^9.0.0",
|
||||
"supertest": "^7.0.0",
|
||||
|
||||
@ -83,7 +83,9 @@ export default {
|
||||
<div v-else-if="message.role === 'tool'">
|
||||
<div class="tool-message-container">
|
||||
<el-collapse>
|
||||
<el-collapse-item title=""></el-collapse-item>
|
||||
<el-collapse-item title="Tool Message">
|
||||
{{ message.args }}
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</div>
|
||||
@ -102,6 +104,17 @@ export default {
|
||||
max-width: min(600px, calc(100% - 60px));
|
||||
}
|
||||
|
||||
.tool-message-container {
|
||||
background-color: antiquewhite;
|
||||
color:black;
|
||||
padding: 10px;
|
||||
margin: 10px 0 10px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
max-width: min(600px, calc(100% - 60px));
|
||||
}
|
||||
|
||||
.user, .assistant {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { OpeyMessage } from "./MessageModel";
|
||||
import type { OpeyMessage, AssistantMessage } from "./MessageModel";
|
||||
|
||||
export interface Chat {
|
||||
messages: OpeyMessage[];
|
||||
currentAssistantMessage: OpeyMessage;
|
||||
currentAssistantMessage: AssistantMessage;
|
||||
status: 'ready' | 'streaming' | 'error' | 'loading';
|
||||
userIsAuthenticated: boolean;
|
||||
threadId: string;
|
||||
|
||||
@ -57,6 +57,7 @@ export interface UserMessage extends OpeyMessage {
|
||||
}
|
||||
|
||||
export interface AssistantMessage extends OpeyMessage {
|
||||
toolCalls: ToolMessage[];
|
||||
// Probably we will need some fields here for tool call/ tool call approval requests
|
||||
}
|
||||
|
||||
|
||||
@ -159,6 +159,7 @@ export const useChat = defineStore('chat', {
|
||||
content: '',
|
||||
role: 'assistant',
|
||||
id: uuidv4(),
|
||||
toolCalls: []
|
||||
}
|
||||
this.addMessage(this.currentAssistantMessage)
|
||||
|
||||
@ -184,13 +185,6 @@ export const useChat = defineStore('chat', {
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Error sending Opey message: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
let context = {
|
||||
currentAssistantMessage: this.currentAssistantMessage,
|
||||
messages: this.messages,
|
||||
status: this.status
|
||||
};
|
||||
|
||||
await this._processOpeyStream(stream);
|
||||
} catch (error) {
|
||||
@ -233,19 +227,21 @@ export const useChat = defineStore('chat', {
|
||||
// This is where we process different types of messages from Opey by their 'type' field
|
||||
// Process pending tool calls
|
||||
if (data.type === 'message') {
|
||||
console.log("Tool Calls: ", content)
|
||||
for (const toolCall of content.tool_calls) {
|
||||
if (content.tool_calls && content.tool_calls.length > 0) {
|
||||
console.log("Tool Calls: ", content)
|
||||
for (const toolCall of content.tool_calls) {
|
||||
|
||||
const toolMessage: ToolMessage = {
|
||||
pending: true,
|
||||
id: uuidv4(),
|
||||
role: 'tool',
|
||||
content: '',
|
||||
awaitingApproval: false,
|
||||
toolCall: toolCall
|
||||
const toolMessage: ToolMessage = {
|
||||
pending: true,
|
||||
id: uuidv4(),
|
||||
role: 'tool',
|
||||
content: '',
|
||||
awaitingApproval: false,
|
||||
toolCall: toolCall
|
||||
}
|
||||
|
||||
this.currentAssistantMessage.toolCalls.push(toolMessage)
|
||||
}
|
||||
|
||||
this.addMessage(toolMessage)
|
||||
}
|
||||
}
|
||||
if (data.type === 'token' && data.content) {
|
||||
@ -255,7 +251,7 @@ export const useChat = defineStore('chat', {
|
||||
this.messages = [...this.messages];
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`Error parsing JSON: ${e}`);
|
||||
throw new Error(`Error parsing JSON: ${e}\n\n${line}`);
|
||||
}
|
||||
} else if (line === 'data: [DONE]') {
|
||||
// Add the current assistant message to the messages list
|
||||
@ -266,6 +262,7 @@ export const useChat = defineStore('chat', {
|
||||
id: '',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
toolCalls: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,6 +214,39 @@ describe('Chat Store _proccessOpeyStream', () => {
|
||||
expect(toolMessage.content).toBe('')
|
||||
})
|
||||
|
||||
it('should associate tool call with current assistant message', async () => {
|
||||
// create a mock stream
|
||||
const mockStream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(`data: {"type": "message", "content": {"type": "ai", "content": "", "tool_calls": [{"name": "retrieve_glossary", "args": {"question": "hre"}, "id": "call_XsmUpPIeS81l9MYpieBZtr4w", "type": "tool_call"}], "tool_approval_request": false, "tool_call_id": null, "run_id": "d0c2bcbe-62f7-464b-8564-bf9263939fe1", "original": {"type": "ai", "data": {"content": "", "additional_kwargs": {"tool_calls": [{"index": 0, "id": "call_XsmUpPIeS81l9MYpieBZtr4w", "function": {"arguments": "{\\"question\\":\\"hre\\"}", "name": "retrieve_glossary"}, "type": "function"}]}, "response_metadata": {"finish_reason": "tool_calls", "model_name": "gpt-4o-2024-08-06", "system_fingerprint": "fp_eb9dce56a8"}, "type": "ai", "name": null, "id": "run-5bb065b9-440d-4678-bbdb-cd6de94a78d3", "example": false, "tool_calls": [{"name": "retrieve_glossary", "args": {"question": "hre"}, "id": "call_XsmUpPIeS81l9MYpieBZtr4w", "type": "tool_call"}], "invalid_tool_calls": [], "usage_metadata": null}}}}\n`));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
// mock the fetch function
|
||||
global.fetch = vi.fn(() =>
|
||||
Promise.resolve(new Response(mockStream, {
|
||||
headers: { 'content-type': 'text/event-stream' },
|
||||
status: 200,
|
||||
}))
|
||||
);
|
||||
|
||||
chatStore.currentAssistantMessage = {
|
||||
id: '123',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
}
|
||||
|
||||
await chatStore._processOpeyStream(mockStream)
|
||||
|
||||
expect(chatStore.messages).toHaveLength(1)
|
||||
|
||||
const toolMessage: ToolMessage = chatStore.messages[0] as ToolMessage
|
||||
|
||||
expect(chatStore.currentAssistantMessage.toolCalls).toBeDefined()
|
||||
expect(chatStore.currentAssistantMessage.toolCalls).toContain(toolMessage.toolCall)
|
||||
})
|
||||
|
||||
it('should throw an error when the chunk is not valid json', async () => {
|
||||
const invalidJsonStream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
|
||||
110
src/test/integration/setup.ts
Normal file
110
src/test/integration/setup.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { createServer as createViteServer } from 'vite';
|
||||
import { afterAll, beforeAll } from 'vitest';
|
||||
import { ChildProcess, spawn } from 'child_process';
|
||||
import fetch from 'node-fetch';
|
||||
import path from 'path';
|
||||
|
||||
// Ports for our test servers
|
||||
const EXPRESS_PORT = 8085; // Match the port in server/app.ts
|
||||
const VITE_PORT = 8086; // Different from the default dev port
|
||||
|
||||
let viteServer: any;
|
||||
let expressServer: ChildProcess;
|
||||
|
||||
/**
|
||||
* Starts the Express and Vue servers for integration testing
|
||||
*/
|
||||
export async function setupTestServers() {
|
||||
// Start Express server as a separate process
|
||||
expressServer = spawn('ts-node', ['server/app.ts'], {
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, PORT: EXPRESS_PORT.toString() }
|
||||
});
|
||||
|
||||
// Log server output for debugging
|
||||
expressServer.stdout?.on('data', (data) => {
|
||||
console.log(`Express server: ${data}`);
|
||||
});
|
||||
|
||||
expressServer.stderr?.on('data', (data) => {
|
||||
console.error(`Express server error: ${data}`);
|
||||
});
|
||||
|
||||
// Start Vite dev server in test mode
|
||||
viteServer = await createViteServer({
|
||||
configFile: path.resolve(__dirname, '../../../vite.config.mts'),
|
||||
server: {
|
||||
port: VITE_PORT,
|
||||
},
|
||||
logLevel: 'silent', // Reduce console noise during tests
|
||||
});
|
||||
|
||||
await viteServer.listen(VITE_PORT);
|
||||
console.log(`Vite test server running at http://localhost:${VITE_PORT}`);
|
||||
|
||||
// Wait for both servers to be fully ready
|
||||
await waitForServer(`http://localhost:${EXPRESS_PORT}/api/status`, 30);
|
||||
await waitForServer(`http://localhost:${VITE_PORT}`, 30);
|
||||
|
||||
return {
|
||||
expressUrl: `http://localhost:${EXPRESS_PORT}`,
|
||||
viteUrl: `http://localhost:${VITE_PORT}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all test servers
|
||||
*/
|
||||
export async function teardownTestServers() {
|
||||
// Close Vite server
|
||||
if (viteServer) {
|
||||
await viteServer.close();
|
||||
}
|
||||
|
||||
// Close Express server
|
||||
if (expressServer) {
|
||||
expressServer.kill('SIGTERM');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to wait for a server to respond
|
||||
*/
|
||||
async function waitForServer(url: string, maxRetries = 30): Promise<boolean> {
|
||||
let retries = 0;
|
||||
|
||||
while (retries < maxRetries) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
// Server not ready yet
|
||||
console.log(`Waiting for ${url} (attempt ${retries + 1}/${maxRetries})...`);
|
||||
}
|
||||
|
||||
retries++;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Increase wait time to 1s
|
||||
}
|
||||
|
||||
throw new Error(`Server at ${url} did not respond in time`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup and teardown hooks for vitest
|
||||
*/
|
||||
export function useIntegrationTestHooks() {
|
||||
let servers: { expressUrl: string; viteUrl: string };
|
||||
|
||||
beforeAll(async () => {
|
||||
servers = await setupTestServers();
|
||||
return servers;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teardownTestServers();
|
||||
});
|
||||
|
||||
return () => servers;
|
||||
}
|
||||
95
src/test/integration/simple.integration.test.ts
Normal file
95
src/test/integration/simple.integration.test.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { describe, test, expect, beforeAll, beforeEach, afterAll, afterEach } from 'vitest';
|
||||
import { useIntegrationTestHooks } from './setup';
|
||||
import { chromium, Browser, Page, BrowserContext } from 'playwright';
|
||||
|
||||
describe('API Explorer Integration Tests', () => {
|
||||
// Setup Express and Vue servers for all tests
|
||||
const getServers = useIntegrationTestHooks();
|
||||
|
||||
let browser: Browser;
|
||||
let context: BrowserContext;
|
||||
let page: Page;
|
||||
|
||||
// Setup browser for testing
|
||||
beforeAll(async () => {
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
// Use this to debug tests visually if needed
|
||||
// headless: false,
|
||||
// slowMo: 1000,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('API status endpoint responds with 200', async () => {
|
||||
const servers = getServers();
|
||||
|
||||
const response = await fetch(`${servers.expressUrl}/api/status`);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data).toHaveProperty('status');
|
||||
});
|
||||
|
||||
// Focus on API tests first since they're less complex
|
||||
test('Backend API endpoints are accessible', async () => {
|
||||
const servers = getServers();
|
||||
|
||||
// Test a few key endpoints
|
||||
const endpoints = [
|
||||
'/api/status',
|
||||
// Add more endpoints as needed
|
||||
];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
const response = await fetch(`${servers.expressUrl}${endpoint}`);
|
||||
expect(response.status).toBe(200);
|
||||
}
|
||||
});
|
||||
|
||||
test('Vite development server is accessible', async () => {
|
||||
const servers = getServers();
|
||||
const response = await fetch(servers.viteUrl);
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
// Comment out the more complex UI tests until the basic setup is working
|
||||
test.skip('Home page loads correctly', async () => {
|
||||
const servers = getServers();
|
||||
await page.goto(servers.viteUrl);
|
||||
|
||||
// Wait for the page to load
|
||||
await page.waitForSelector('title');
|
||||
|
||||
// Check that the title contains expected text
|
||||
const title = await page.title();
|
||||
expect(title).toContain('API Explorer');
|
||||
});
|
||||
|
||||
test.skip('Chat widget can be opened', async () => {
|
||||
const servers = getServers();
|
||||
await page.goto(servers.viteUrl);
|
||||
|
||||
// Find and click the chat widget button
|
||||
const chatButton = await page.waitForSelector('.chat-widget-button');
|
||||
await chatButton.click();
|
||||
|
||||
// Check that the chat container appears
|
||||
await page.waitForSelector('.chat-container');
|
||||
|
||||
const chatContainerExists = await page.isVisible('.chat-container');
|
||||
expect(chatContainerExists).toBe(true);
|
||||
});
|
||||
});
|
||||
18
vitest.integration.config.js
Normal file
18
vitest.integration.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
testTimeout: 60000, // Integration tests need more time
|
||||
hookTimeout: 60000,
|
||||
include: ['src/test/integration/**/*.integration.test.ts'],
|
||||
setupFiles: ['src/test/integration/setup.ts'],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user