Spaces:
Running
Running
| import { jest } from '@jest/globals'; | |
| import express from 'express'; | |
| import request from 'supertest'; | |
| const buildAppWithRoute = async () => { | |
| const { default: router } = await import(`../routes/aiModelRoutes.js?test=${Date.now()}`); | |
| const app = express(); | |
| app.use(express.json()); | |
| app.use('/api/ml', router); | |
| return app; | |
| }; | |
| describe('AI model route proxy failover', () => { | |
| const originalEnv = { ...process.env }; | |
| const originalFetch = global.fetch; | |
| afterEach(() => { | |
| process.env = { ...originalEnv }; | |
| global.fetch = originalFetch; | |
| jest.resetModules(); | |
| }); | |
| it('falls back to inferred HF AI backend when localhost fails', async () => { | |
| process.env.AI_BACKEND_URL = ''; | |
| process.env.SPACE_HOST = 'arko007-agromind-backend.hf.space'; | |
| const calls = []; | |
| global.fetch = jest.fn(async (url) => { | |
| calls.push(url); | |
| if (String(url).startsWith('https://arko007-agromind-ai-backend.hf.space')) { | |
| return { | |
| status: 200, | |
| text: async () => JSON.stringify({ crop: 'rice' }), | |
| }; | |
| } | |
| throw new Error('connect ECONNREFUSED 127.0.0.1:5000'); | |
| }); | |
| const app = await buildAppWithRoute(); | |
| const response = await request(app) | |
| .post('/api/ml/crop-recommendation') | |
| .send({ N: 33, P: 36, K: 62 }); | |
| expect(response.status).toBe(200); | |
| expect(response.body.crop).toBe('rice'); | |
| expect(calls[0]).toBe('https://arko007-agromind-ai-backend.hf.space/crop_recommendation'); | |
| }); | |
| it('returns diagnostic details when all candidates fail', async () => { | |
| process.env.AI_BACKEND_URL = 'https://invalid-ai-service.example.com'; | |
| process.env.SPACE_HOST = 'arko007-agromind-backend.hf.space'; | |
| global.fetch = jest.fn(async () => { | |
| throw new Error('upstream unavailable'); | |
| }); | |
| const app = await buildAppWithRoute(); | |
| const response = await request(app) | |
| .post('/api/ml/crop-recommendation') | |
| .send({ N: 33, P: 36, K: 62 }); | |
| expect(response.status).toBe(502); | |
| expect(response.body.error).toBe('AI model service unavailable'); | |
| expect(Array.isArray(response.body.details)).toBe(true); | |
| expect(response.body.details.length).toBeGreaterThan(0); | |
| }); | |
| }); | |
| describe('AI analysis endpoint', () => { | |
| it('returns 400 when model_type or prediction is missing', async () => { | |
| const app = await buildAppWithRoute(); | |
| const res1 = await request(app) | |
| .post('/api/ml/analyze-prediction') | |
| .send({ prediction: 'Rice' }); | |
| expect(res1.status).toBe(400); | |
| expect(res1.body.error).toMatch(/model_type/); | |
| const res2 = await request(app) | |
| .post('/api/ml/analyze-prediction') | |
| .send({ model_type: 'crop_recommendation' }); | |
| expect(res2.status).toBe(400); | |
| expect(res2.body.error).toMatch(/prediction/); | |
| }); | |
| it('returns explicit error without fallback analysis when GROQ_API_KEY is missing', async () => { | |
| const origKey = process.env.GROQ_API_KEY; | |
| delete process.env.GROQ_API_KEY; | |
| const app = await buildAppWithRoute(); | |
| const res = await request(app) | |
| .post('/api/ml/analyze-prediction') | |
| .send({ | |
| model_type: 'crop_recommendation', | |
| prediction: 'Rice', | |
| input_data: { nitrogen: 50, phosphorus: 30, potassium: 40 }, | |
| }); | |
| // Without the API key the handler should return 500 without synthetic fallback output | |
| expect(res.status).toBe(500); | |
| expect(res.body.success).toBe(false); | |
| expect(res.body.error).toBe('Failed to generate analysis'); | |
| expect(res.body.analysis).toBeUndefined(); | |
| process.env.GROQ_API_KEY = origKey; | |
| }); | |
| }); | |
| describe('New ML model proxy routes', () => { | |
| const originalEnv = { ...process.env }; | |
| const originalFetch = global.fetch; | |
| afterEach(() => { | |
| process.env = { ...originalEnv }; | |
| global.fetch = originalFetch; | |
| jest.resetModules(); | |
| }); | |
| it('walnut-rancidity forwards JSON to AI backend', async () => { | |
| process.env.AI_BACKEND_URL = ''; | |
| process.env.SPACE_HOST = 'arko007-agromind-backend.hf.space'; | |
| global.fetch = jest.fn(async (url) => { | |
| if (String(url).includes('/walnut_rancidity_predict')) { | |
| return { | |
| status: 200, | |
| text: async () => JSON.stringify({ | |
| model: 'walnut-rancidity-predictor', | |
| prediction: { rancidity_probability: 0.15, shelf_life_remaining_days: 120 }, | |
| risk_level: 'LOW', | |
| }), | |
| }; | |
| } | |
| throw new Error('connect ECONNREFUSED'); | |
| }); | |
| const app = await buildAppWithRoute(); | |
| const response = await request(app) | |
| .post('/api/ml/walnut-rancidity') | |
| .send({ storage_days: 10, temperature: 5, humidity: 50, moisture: 4 }); | |
| expect(response.status).toBe(200); | |
| expect(response.body.risk_level).toBe('LOW'); | |
| }); | |
| it('apple-price forwards JSON to AI backend', async () => { | |
| process.env.AI_BACKEND_URL = ''; | |
| process.env.SPACE_HOST = 'arko007-agromind-backend.hf.space'; | |
| global.fetch = jest.fn(async (url) => { | |
| if (String(url).includes('/apple_price_predict')) { | |
| return { | |
| status: 200, | |
| text: async () => JSON.stringify({ | |
| model: 'apple-price-predictor', | |
| predicted_price_7d: 127.5, | |
| recommendation: 'STORE', | |
| }), | |
| }; | |
| } | |
| throw new Error('connect ECONNREFUSED'); | |
| }); | |
| const app = await buildAppWithRoute(); | |
| const response = await request(app) | |
| .post('/api/ml/apple-price') | |
| .send({ current_price: 120, apple_variety: 'Kinnauri', region: 'Himachal Pradesh' }); | |
| expect(response.status).toBe(200); | |
| expect(response.body.recommendation).toBe('STORE'); | |
| }); | |
| it('saffron-classify returns 400 when no file is provided', async () => { | |
| const app = await buildAppWithRoute(); | |
| const response = await request(app) | |
| .post('/api/ml/saffron-classify') | |
| .send({}); | |
| expect(response.status).toBe(400); | |
| expect(response.body.error).toMatch(/image/i); | |
| }); | |
| it('walnut-defect returns 400 when no file is provided', async () => { | |
| const app = await buildAppWithRoute(); | |
| const response = await request(app) | |
| .post('/api/ml/walnut-defect') | |
| .send({}); | |
| expect(response.status).toBe(400); | |
| expect(response.body.error).toMatch(/image/i); | |
| }); | |
| }); | |