1+ import { describe , it , expect , beforeEach , afterEach } from 'vitest' ;
2+ import { execa } from 'execa' ;
3+ import fs from 'fs' ;
4+ import path from 'path' ;
5+ import http from 'http' ;
6+
7+ describe ( 'Resource Monitoring Integration' , ( ) => {
8+ const testWorkspace = path . join ( process . cwd ( ) , 'test-workspace' , 'integration-test' ) ;
9+ let adminProcess = null ;
10+
11+ beforeEach ( async ( ) => {
12+ // Clean up previous test workspace
13+ if ( fs . existsSync ( testWorkspace ) ) {
14+ fs . rmSync ( testWorkspace , { recursive : true , force : true } ) ;
15+ }
16+
17+ // Create test workspace
18+ fs . mkdirSync ( testWorkspace , { recursive : true } ) ;
19+
20+ // Create a simple polyglot project
21+ const polyglotConfig = {
22+ name : 'integration-test' ,
23+ services : [
24+ { name : 'test-node' , type : 'node' , port : 4001 , path : 'services/test-node' }
25+ ]
26+ } ;
27+
28+ fs . writeFileSync (
29+ path . join ( testWorkspace , 'polyglot.json' ) ,
30+ JSON . stringify ( polyglotConfig , null , 2 )
31+ ) ;
32+
33+ // Create service directory structure
34+ const serviceDir = path . join ( testWorkspace , 'services' , 'test-node' ) ;
35+ fs . mkdirSync ( serviceDir , { recursive : true } ) ;
36+
37+ // Create basic package.json for the service
38+ const packageJson = {
39+ name : 'test-node' ,
40+ version : '1.0.0' ,
41+ scripts : {
42+ dev : 'node index.js' ,
43+ start : 'node index.js'
44+ } ,
45+ dependencies : { }
46+ } ;
47+
48+ fs . writeFileSync (
49+ path . join ( serviceDir , 'package.json' ) ,
50+ JSON . stringify ( packageJson , null , 2 )
51+ ) ;
52+
53+ // Create basic service file
54+ const serviceCode = `
55+ const http = require('http');
56+ const port = process.env.PORT || 4001;
57+
58+ const server = http.createServer((req, res) => {
59+ if (req.url === '/health') {
60+ res.writeHead(200, { 'Content-Type': 'application/json' });
61+ res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() }));
62+ } else {
63+ res.writeHead(200, { 'Content-Type': 'text/plain' });
64+ res.end('Test Node Service Running');
65+ }
66+ });
67+
68+ server.listen(port, () => {
69+ console.log(\`Test service running on port \${port}\`);
70+ });
71+
72+ process.on('SIGTERM', () => {
73+ console.log('Received SIGTERM, shutting down gracefully');
74+ server.close(() => {
75+ process.exit(0);
76+ });
77+ });
78+ ` ;
79+
80+ fs . writeFileSync ( path . join ( serviceDir , 'index.js' ) , serviceCode ) ;
81+ } ) ;
82+
83+ afterEach ( async ( ) => {
84+ // Stop admin dashboard if running
85+ if ( adminProcess && ! adminProcess . killed ) {
86+ try {
87+ adminProcess . kill ( 'SIGTERM' ) ;
88+ // Give the process time to shut down gracefully
89+ await Promise . race ( [
90+ adminProcess . catch ( ( ) => { } ) , // Ignore errors during cleanup
91+ new Promise ( resolve => setTimeout ( resolve , 2000 ) )
92+ ] ) ;
93+ } catch ( error ) {
94+ // Force kill if graceful shutdown fails
95+ try {
96+ adminProcess . kill ( 'SIGKILL' ) ;
97+ } catch ( e ) {
98+ // Ignore errors during force kill
99+ }
100+ }
101+ }
102+ adminProcess = null ;
103+
104+ // Clean up test workspace
105+ if ( fs . existsSync ( testWorkspace ) ) {
106+ fs . rmSync ( testWorkspace , { recursive : true , force : true } ) ;
107+ }
108+ } ) ;
109+
110+ it ( 'should start admin dashboard with resource monitoring enabled' , async ( ) => {
111+ // Start admin dashboard
112+ adminProcess = execa ( 'node' , [
113+ path . join ( process . cwd ( ) , 'bin' , 'index.js' ) ,
114+ 'admin' ,
115+ '--port' , '9999'
116+ ] , {
117+ cwd : testWorkspace ,
118+ stdio : 'pipe'
119+ } ) ;
120+
121+ // Wait for dashboard to start
122+ await new Promise ( resolve => setTimeout ( resolve , 3000 ) ) ;
123+
124+ expect ( adminProcess . killed ) . toBe ( false ) ;
125+
126+ // Test if dashboard is responding
127+ const response = await makeRequest ( 'GET' , 'http://localhost:9999/' , { } , 5000 ) ;
128+ expect ( response . statusCode ) . toBe ( 200 ) ;
129+ expect ( response . body ) . toContain ( 'Polyglot Admin Dashboard' ) ;
130+
131+ // Test if resource monitoring UI is included
132+ expect ( response . body ) . toContain ( 'Resource Monitoring' ) ;
133+ expect ( response . body ) . toContain ( 'CPU Usage' ) ;
134+ expect ( response . body ) . toContain ( 'Memory Usage' ) ;
135+ expect ( response . body ) . toContain ( 'Network I/O' ) ;
136+ expect ( response . body ) . toContain ( 'Disk I/O' ) ;
137+ } , 15000 ) ;
138+
139+ it ( 'should provide metrics API endpoint' , async ( ) => {
140+ // Start admin dashboard
141+ adminProcess = execa ( 'node' , [
142+ path . join ( process . cwd ( ) , 'bin' , 'index.js' ) ,
143+ 'admin' ,
144+ '--port' , '9998'
145+ ] , {
146+ cwd : testWorkspace ,
147+ stdio : 'pipe'
148+ } ) ;
149+
150+ // Wait for dashboard to start
151+ await new Promise ( resolve => setTimeout ( resolve , 3000 ) ) ;
152+
153+ // Test metrics API endpoint
154+ const response = await makeRequest ( 'GET' , 'http://localhost:9998/api/metrics' ) ;
155+ expect ( response . statusCode ) . toBe ( 200 ) ;
156+
157+ const data = JSON . parse ( response . body ) ;
158+ expect ( data ) . toHaveProperty ( 'metrics' ) ;
159+ expect ( data ) . toHaveProperty ( 'systemInfo' ) ;
160+
161+ if ( data . systemInfo ) {
162+ expect ( data . systemInfo ) . toHaveProperty ( 'cpu' ) ;
163+ expect ( data . systemInfo ) . toHaveProperty ( 'memory' ) ;
164+ }
165+ } , 15000 ) ;
166+
167+ it ( 'should provide service status API with resource monitoring integration' , async ( ) => {
168+ // Start admin dashboard
169+ adminProcess = execa ( 'node' , [
170+ path . join ( process . cwd ( ) , 'bin' , 'index.js' ) ,
171+ 'admin' ,
172+ '--port' , '9997'
173+ ] , {
174+ cwd : testWorkspace ,
175+ stdio : 'pipe'
176+ } ) ;
177+
178+ // Wait for dashboard to start
179+ await new Promise ( resolve => setTimeout ( resolve , 3000 ) ) ;
180+
181+ // Test service status API
182+ const response = await makeRequest ( 'GET' , 'http://localhost:9997/api/status' ) ;
183+ expect ( response . statusCode ) . toBe ( 200 ) ;
184+
185+ const services = JSON . parse ( response . body ) ;
186+ expect ( Array . isArray ( services ) ) . toBe ( true ) ;
187+ expect ( services . length ) . toBeGreaterThan ( 0 ) ;
188+
189+ const service = services [ 0 ] ;
190+ expect ( service ) . toHaveProperty ( 'name' ) ;
191+ expect ( service ) . toHaveProperty ( 'type' ) ;
192+ expect ( service ) . toHaveProperty ( 'port' ) ;
193+ expect ( service ) . toHaveProperty ( 'status' ) ;
194+ } , 15000 ) ;
195+
196+ it ( 'should handle graceful shutdown without errors' , async ( ) => {
197+ // Start admin dashboard
198+ adminProcess = execa ( 'node' , [
199+ path . join ( process . cwd ( ) , 'bin' , 'index.js' ) ,
200+ 'admin' ,
201+ '--port' , '9996'
202+ ] , {
203+ cwd : testWorkspace ,
204+ stdio : 'pipe'
205+ } ) ;
206+
207+ // Wait for dashboard to start
208+ await new Promise ( resolve => setTimeout ( resolve , 3000 ) ) ;
209+
210+ // Send SIGTERM to gracefully shut down
211+ adminProcess . kill ( 'SIGTERM' ) ;
212+
213+ // Wait for process to exit with timeout
214+ try {
215+ const result = await Promise . race ( [
216+ adminProcess ,
217+ new Promise ( ( _ , reject ) => setTimeout ( ( ) => reject ( new Error ( 'Timeout' ) ) , 10000 ) )
218+ ] ) ;
219+
220+ // Process should exit (either gracefully or terminated)
221+ expect ( adminProcess . killed || result . exitCode !== undefined ) . toBe ( true ) ;
222+ } catch ( error ) {
223+ // If timeout or termination, that's acceptable in test environment
224+ expect ( error . message === 'Timeout' || error . signal === 'SIGTERM' || error . signal === 'SIGKILL' ) . toBe ( true ) ;
225+ }
226+ } , 15000 ) ;
227+
228+ it ( 'should include Chart.js library for metrics visualization' , async ( ) => {
229+ // Start admin dashboard
230+ adminProcess = execa ( 'node' , [
231+ path . join ( process . cwd ( ) , 'bin' , 'index.js' ) ,
232+ 'admin' ,
233+ '--port' , '9995'
234+ ] , {
235+ cwd : testWorkspace ,
236+ stdio : 'pipe'
237+ } ) ;
238+
239+ // Wait for dashboard to start
240+ await new Promise ( resolve => setTimeout ( resolve , 3000 ) ) ;
241+
242+ // Test if Chart.js is included in the HTML
243+ const response = await makeRequest ( 'GET' , 'http://localhost:9995/' ) ;
244+ expect ( response . statusCode ) . toBe ( 200 ) ;
245+ expect ( response . body ) . toContain ( 'chart.js' ) ;
246+ expect ( response . body ) . toContain ( 'canvas id="cpu-chart"' ) ;
247+ expect ( response . body ) . toContain ( 'canvas id="memory-chart"' ) ;
248+ expect ( response . body ) . toContain ( 'canvas id="network-chart"' ) ;
249+ expect ( response . body ) . toContain ( 'canvas id="disk-chart"' ) ;
250+ } , 15000 ) ;
251+ } ) ;
252+
253+ // Helper function to make HTTP requests with timeout
254+ function makeRequest ( method , url , data = null , timeout = 10000 ) {
255+ return new Promise ( ( resolve , reject ) => {
256+ const urlObj = new URL ( url ) ;
257+ const options = {
258+ hostname : urlObj . hostname ,
259+ port : urlObj . port ,
260+ path : urlObj . pathname + urlObj . search ,
261+ method : method ,
262+ timeout : timeout ,
263+ headers : {
264+ 'User-Agent' : 'test-client' ,
265+ }
266+ } ;
267+
268+ if ( data && method !== 'GET' ) {
269+ const postData = typeof data === 'string' ? data : JSON . stringify ( data ) ;
270+ options . headers [ 'Content-Type' ] = 'application/json' ;
271+ options . headers [ 'Content-Length' ] = Buffer . byteLength ( postData ) ;
272+ }
273+
274+ const req = http . request ( options , ( res ) => {
275+ let body = '' ;
276+ res . on ( 'data' , ( chunk ) => body += chunk ) ;
277+ res . on ( 'end' , ( ) => {
278+ resolve ( {
279+ statusCode : res . statusCode ,
280+ headers : res . headers ,
281+ body : body
282+ } ) ;
283+ } ) ;
284+ } ) ;
285+
286+ req . on ( 'error' , reject ) ;
287+ req . on ( 'timeout' , ( ) => {
288+ req . destroy ( ) ;
289+ reject ( new Error ( `Request timeout after ${ timeout } ms` ) ) ;
290+ } ) ;
291+
292+ if ( data && method !== 'GET' ) {
293+ req . write ( typeof data === 'string' ? data : JSON . stringify ( data ) ) ;
294+ }
295+
296+ req . end ( ) ;
297+ } ) ;
298+ }
0 commit comments