Agromind-backend / backend /tests /aiModelRoutes.test.js
gh-action-hf-auto
auto: sync backend from github@32fb9685
8a6248c
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);
});
});