-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathsystemTest.relay.spec.ts
More file actions
187 lines (163 loc) · 7.21 KB
/
Copy pathsystemTest.relay.spec.ts
File metadata and controls
187 lines (163 loc) · 7.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/**
* System test: verifies that the relay correctly proxies AI requests from MetaConfigurator
* to an upstream LLM endpoint, injecting the provider API key server-side.
*
* The test starts:
* - a mock LLM server (Node.js, port 9999) that echoes the user prompt as JSON
* - the MetaConfigurator relay (Python, port 8081) pointing at the mock LLM
*
* MetaConfigurator is configured (via settings fixture) to route AI calls through the relay.
* The test enters a schema-creation prompt, submits it, and asserts that the resulting
* schema document equals the echo response the mock LLM produced.
*/
import {test, expect} from '../../../tests/shared/playwright';
import * as http from 'node:http';
import {ChildProcess, spawn} from 'node:child_process';
import * as path from 'node:path';
import {openApp, forceEditorMode} from '../../../tests/shared/utils';
import {SessionMode} from '../../../meta_configurator/src/store/sessionMode';
import {tpGetData} from '../../../tests/shared/utilsTestPanel';
import {aiPanelEnterCreatePrompt, aiPanelSubmitCreate} from '../../../tests/shared/utilsAiPanel';
const MOCK_LLM_PORT = 9999;
const RELAY_PORT = 18081;
const RELAY_DIR = path.join(__dirname, '..');
const RELAY_CONFIG = path.join(__dirname, 'relay-e2e-config.yaml');
let mockLlmServer: http.Server | null = null;
let relayProcess: ChildProcess | null = null;
let relayStartupError = '';
// ---------------------------------------------------------------------------
// Mock LLM server — echoes the user message content wrapped in {"request": ...}
// ---------------------------------------------------------------------------
function startMockLlmServer(): Promise<void> {
return new Promise((resolve, reject) => {
mockLlmServer = http.createServer((req, res) => {
if (req.method !== 'POST' || req.url !== '/chat/completions') {
res.writeHead(404);
res.end('Not found');
return;
}
let body = '';
req.on('data', chunk => (body += chunk));
req.on('end', () => {
try {
const parsed = JSON.parse(body);
const messages: Array<{role: string; content: string}> = parsed.messages ?? [];
const userMsg = [...messages].reverse().find(m => m.role === 'user');
const userContent = userMsg?.content ?? '';
const replyContent = JSON.stringify({request: userContent});
const reply = JSON.stringify({
id: 'mock-1',
object: 'chat.completion',
choices: [
{
index: 0,
message: {role: 'assistant', content: replyContent},
finish_reason: 'stop',
},
],
usage: {prompt_tokens: 5, completion_tokens: 5, total_tokens: 10},
});
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(reply);
} catch (e) {
res.writeHead(400);
res.end('Bad request');
}
});
});
mockLlmServer.on('error', reject);
mockLlmServer.listen(MOCK_LLM_PORT, '127.0.0.1', () => resolve());
});
}
function stopMockLlmServer(): Promise<void> {
return new Promise(resolve => {
if (!mockLlmServer) return resolve();
mockLlmServer.close(() => resolve());
});
}
// ---------------------------------------------------------------------------
// Relay process
// ---------------------------------------------------------------------------
function startRelay(): Promise<void> {
return new Promise((resolve, reject) => {
const python = process.platform === 'win32' ? 'python' : 'python3';
relayStartupError = '';
relayProcess = spawn(python, ['app.py'], {
cwd: RELAY_DIR,
env: {
...process.env,
CONFIG_PATH: RELAY_CONFIG,
HOST: '127.0.0.1',
PORT: String(RELAY_PORT),
},
});
relayProcess.stdout?.on('data', data => process.stderr.write(`[relay] ${data}`));
relayProcess.stderr?.on('data', data => {
const text = String(data);
relayStartupError += text;
process.stderr.write(`[relay] ${text}`);
});
relayProcess.on('error', err => {
relayStartupError += String(err);
process.stderr.write(`[relay spawn error] ${err}\n`);
});
const deadline = Date.now() + 15000;
const poll = () => {
if (relayProcess?.exitCode !== null && relayProcess?.exitCode !== undefined) {
reject(new Error(`Relay exited early with code ${relayProcess.exitCode}: ${relayStartupError.trim()}`));
return;
}
const req = http.get(`http://127.0.0.1:${RELAY_PORT}/health`, res => {
if (res.statusCode === 200) {
resolve();
} else if (Date.now() < deadline) {
setTimeout(poll, 300);
} else {
reject(new Error('Relay health check returned non-200'));
}
res.resume();
});
req.on('error', () => {
if (Date.now() < deadline) setTimeout(poll, 300);
else reject(new Error(`Relay failed to start within 15 s: ${relayStartupError.trim()}`));
});
};
setTimeout(poll, 500);
});
}
function stopRelay(): void {
if (relayProcess) {
relayProcess.kill();
relayProcess = null;
}
}
// ---------------------------------------------------------------------------
// Test lifecycle
// ---------------------------------------------------------------------------
test.beforeAll(async () => {
await startMockLlmServer();
await startRelay();
});
test.afterAll(async () => {
stopRelay();
await stopMockLlmServer();
});
// ---------------------------------------------------------------------------
// The system test
// ---------------------------------------------------------------------------
test('relay proxies schema-creation prompt and applies the response as the new schema', async ({page}) => {
test.setTimeout(30000);
const prompt = 'create a simple person schema';
// Load MC in schema editor mode with relay settings and an empty schema.
// Providing a schema fixture (even an empty one) skips the initial schema-selection dialog.
await openApp(page, 'settings_relay_test.json', null, 'schema_empty.json');
await forceEditorMode(page, SessionMode.SchemaEditor);
// Enter the prompt in the AI panel's "Create Schema" form and submit.
await aiPanelEnterCreatePrompt(page, prompt);
await aiPanelSubmitCreate(page);
// Poll until the schema document equals the echo response from the mock LLM.
// The mock wraps the user message in {"request": "<prompt>"}.
await expect
.poll(() => tpGetData(page, SessionMode.SchemaEditor), {timeout: 15000})
.toEqual({request: prompt});
});