refactor: enhance type safety and functionality in AI routes and tests
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m27s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 4m27s
This commit is contained in:
@@ -27,7 +27,7 @@ describe('AnalysisPanel', () => {
|
||||
mockedAiApiClient.getQuickInsights.mockReset();
|
||||
mockedAiApiClient.getDeepDiveAnalysis.mockReset();
|
||||
mockedAiApiClient.searchWeb.mockReset();
|
||||
// mockedAiApiClient.planTripWithMaps.mockReset();
|
||||
mockedAiApiClient.planTripWithMaps.mockReset();
|
||||
mockedAiApiClient.generateImageFromText.mockReset();
|
||||
|
||||
// Mock Geolocation API
|
||||
|
||||
@@ -356,17 +356,16 @@ router.post('/search-web', passport.authenticate('jwt', { session: false }), asy
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/plan-trip', passport.authenticate('jwt', { session: false }), async (req, res) => {
|
||||
// try {
|
||||
// const { items, store, userLocation } = req.body;
|
||||
// logger.info(`Server-side trip planning requested for user.`);
|
||||
// const result = await aiService.planTripWithMaps(items, store, userLocation);
|
||||
// res.status(200).json(result);
|
||||
// } catch (error) {
|
||||
// logger.error('Error in /api/ai/plan-trip endpoint:', { error });
|
||||
// next(error);
|
||||
// }
|
||||
res.status(501).json({ message: 'This feature is currently disabled.' });
|
||||
router.post('/plan-trip', passport.authenticate('jwt', { session: false }), async (req, res, next: NextFunction) => {
|
||||
try {
|
||||
const { items, store, userLocation } = req.body;
|
||||
logger.info(`Server-side trip planning requested for user.`);
|
||||
const result = await aiService.planTripWithMaps(items, store, userLocation);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
logger.error('Error in /api/ai/plan-trip endpoint:', { error });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// --- STUBBED AI Routes for Future Features ---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/routes/auth.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import express, { Request } from 'express';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import authRouter from './auth';
|
||||
@@ -89,7 +89,6 @@ describe('Auth Routes (/api/auth)', () => {
|
||||
};
|
||||
|
||||
vi.mocked(db.findUserByEmail).mockResolvedValue(undefined); // No existing user
|
||||
// @ts-ignore
|
||||
vi.mocked(db.createUser).mockResolvedValue(mockNewUser);
|
||||
vi.mocked(db.saveRefreshToken).mockResolvedValue(undefined);
|
||||
vi.mocked(db.logActivity).mockResolvedValue(undefined);
|
||||
@@ -126,8 +125,13 @@ describe('Auth Routes (/api/auth)', () => {
|
||||
|
||||
it('should reject registration if the email already exists', async () => {
|
||||
// Arrange: Mock that the user exists
|
||||
// @ts-ignore
|
||||
vi.mocked(db.findUserByEmail).mockResolvedValue({ user_id: 'existing', email: newUserEmail });
|
||||
vi.mocked(db.findUserByEmail).mockResolvedValue({
|
||||
user_id: 'existing',
|
||||
email: newUserEmail,
|
||||
password_hash: 'some_hash',
|
||||
failed_login_attempts: 0,
|
||||
last_failed_login: null,
|
||||
});
|
||||
|
||||
// Act
|
||||
const response = await supertest(app)
|
||||
@@ -198,8 +202,13 @@ describe('Auth Routes (/api/auth)', () => {
|
||||
describe('POST /forgot-password', () => {
|
||||
it('should send a reset link if the user exists', async () => {
|
||||
// Arrange
|
||||
// @ts-ignore
|
||||
vi.mocked(db.findUserByEmail).mockResolvedValue({ user_id: 'user-123', email: 'test@test.com' });
|
||||
vi.mocked(db.findUserByEmail).mockResolvedValue({
|
||||
user_id: 'user-123',
|
||||
email: 'test@test.com',
|
||||
password_hash: 'some_hash',
|
||||
failed_login_attempts: 0,
|
||||
last_failed_login: null,
|
||||
});
|
||||
vi.mocked(db.createPasswordResetToken).mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
@@ -232,8 +241,7 @@ describe('Auth Routes (/api/auth)', () => {
|
||||
describe('POST /reset-password', () => {
|
||||
it('should reset the password with a valid token and strong password', async () => {
|
||||
// Arrange
|
||||
const tokenRecord = { user_id: 'user-123', token_hash: 'hashed-token', expires_at: new Date(Date.now() + 3600000).toISOString() };
|
||||
// @ts-ignore
|
||||
const tokenRecord = { user_id: 'user-123', token_hash: 'hashed-token', expires_at: new Date(Date.now() + 3600000) };
|
||||
vi.mocked(db.getValidResetTokens).mockResolvedValue([tokenRecord]);
|
||||
vi.mocked(bcrypt.compare).mockResolvedValue(true as never); // Token matches
|
||||
vi.mocked(db.updateUserPassword).mockResolvedValue(undefined);
|
||||
@@ -268,8 +276,13 @@ describe('Auth Routes (/api/auth)', () => {
|
||||
describe('POST /refresh-token', () => {
|
||||
it('should issue a new access token with a valid refresh token cookie', async () => {
|
||||
// Arrange
|
||||
const mockUser = { user_id: 'user-123', email: 'test@test.com' };
|
||||
// @ts-ignore
|
||||
const mockUser = {
|
||||
user_id: 'user-123',
|
||||
email: 'test@test.com',
|
||||
password_hash: 'some_hash',
|
||||
failed_login_attempts: 0,
|
||||
last_failed_login: null,
|
||||
};
|
||||
vi.mocked(db.findUserByRefreshToken).mockResolvedValue(mockUser);
|
||||
|
||||
// Act
|
||||
|
||||
@@ -5,7 +5,7 @@ import express from 'express';
|
||||
import publicRouter from './public'; // Import the router we want to test
|
||||
import * as db from '../services/db';
|
||||
import * as fs from 'fs/promises';
|
||||
import { Flyer } from '../types';
|
||||
import { Flyer, Recipe } from '../types';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
// This decouples the route tests from the SQL implementation details.
|
||||
@@ -141,7 +141,6 @@ describe('Public Routes (/api)', () => {
|
||||
describe('GET /master-items', () => {
|
||||
it('should return a list of master items', async () => {
|
||||
const mockItems = [{ master_grocery_item_id: 1, name: 'Milk', created_at: new Date().toISOString() }];
|
||||
// @ts-ignore
|
||||
vi.mocked(db.getAllMasterItems).mockResolvedValue(mockItems);
|
||||
|
||||
const response = await supertest(app).get('/api/master-items');
|
||||
@@ -156,7 +155,6 @@ describe('Public Routes (/api)', () => {
|
||||
const mockFlyerItems = [
|
||||
{ flyer_item_id: 1, flyer_id: 123, item: 'Cheese', price_display: '$5', price_in_cents: 500, created_at: new Date().toISOString(), view_count: 0, click_count: 0, updated_at: new Date().toISOString(), quantity: '500g' }
|
||||
];
|
||||
// @ts-ignore
|
||||
vi.mocked(db.getFlyerItems).mockResolvedValue(mockFlyerItems);
|
||||
|
||||
const response = await supertest(app).get('/api/flyers/123/items');
|
||||
@@ -171,7 +169,6 @@ describe('Public Routes (/api)', () => {
|
||||
const mockFlyerItems = [
|
||||
{ flyer_item_id: 1, flyer_id: 1, item: 'Bread', price_display: '$2', price_in_cents: 200, created_at: new Date().toISOString(), view_count: 0, click_count: 0, updated_at: new Date().toISOString(), quantity: '1 loaf' }
|
||||
];
|
||||
// @ts-ignore
|
||||
vi.mocked(db.getFlyerItemsForFlyers).mockResolvedValue(mockFlyerItems);
|
||||
|
||||
const response = await supertest(app)
|
||||
@@ -192,11 +189,10 @@ describe('Public Routes (/api)', () => {
|
||||
|
||||
describe('GET /recipes/by-sale-percentage', () => {
|
||||
it('should return recipes based on sale percentage', async () => {
|
||||
const mockRecipes = [
|
||||
{ recipe_id: 1, name: 'Pasta', avg_rating: 0, rating_count: 0, fork_count: 0, status: 'public', created_at: new Date().toISOString() }
|
||||
const mockRecipes: Recipe[] = [
|
||||
{ recipe_id: 1, name: 'Pasta', description: null, instructions: null, avg_rating: 0, rating_count: 0, fork_count: 0, status: 'public', created_at: new Date().toISOString() }
|
||||
];
|
||||
// @ts-ignore
|
||||
vi.mocked(db.getRecipesBySalePercentage).mockResolvedValue(mockRecipes);
|
||||
vi.mocked(db.getRecipesBySalePercentage).mockResolvedValue(mockRecipes as any);
|
||||
|
||||
const response = await supertest(app).get('/api/recipes/by-sale-percentage?minPercentage=75');
|
||||
|
||||
@@ -243,11 +239,10 @@ describe('Public Routes (/api)', () => {
|
||||
|
||||
describe('GET /recipes/by-ingredient-and-tag', () => {
|
||||
it('should return recipes for a given ingredient and tag', async () => {
|
||||
const mockRecipes = [
|
||||
{ recipe_id: 2, name: 'Chicken Tacos', avg_rating: 0, rating_count: 0, fork_count: 0, status: 'public', created_at: new Date().toISOString() }
|
||||
const mockRecipes: Recipe[] = [
|
||||
{ recipe_id: 2, name: 'Chicken Tacos', description: null, instructions: null, avg_rating: 0, rating_count: 0, fork_count: 0, status: 'public', created_at: new Date().toISOString() }
|
||||
];
|
||||
// @ts-ignore
|
||||
vi.mocked(db.findRecipesByIngredientAndTag).mockResolvedValue(mockRecipes);
|
||||
vi.mocked(db.findRecipesByIngredientAndTag).mockResolvedValue(mockRecipes as any);
|
||||
|
||||
const response = await supertest(app).get('/api/recipes/by-ingredient-and-tag?ingredient=chicken&tag=quick');
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import express from 'express';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import userRouter from './user';
|
||||
import * as db from '../services/db';
|
||||
import { UserProfile } from '../types';
|
||||
import { UserProfile, MasterGroceryItem, ShoppingList, ShoppingListItem, Appliance } from '../types';
|
||||
|
||||
// 1. Mock the Service Layer directly.
|
||||
vi.mock('../services/db');
|
||||
@@ -119,8 +119,13 @@ describe('User Routes (/api/users)', () => {
|
||||
describe('GET /watched-items', () => {
|
||||
it('should return a list of watched items', async () => {
|
||||
// Arrange
|
||||
const mockItems = [{ master_grocery_item_id: 1, name: 'Milk', created_at: new Date().toISOString() }];
|
||||
// @ts-ignore
|
||||
const mockItems: MasterGroceryItem[] = [{
|
||||
master_grocery_item_id: 1,
|
||||
name: 'Milk',
|
||||
created_at: new Date().toISOString(),
|
||||
category_id: 1, // Add missing properties
|
||||
category_name: 'Dairy & Eggs'
|
||||
}];
|
||||
vi.mocked(db.getWatchedItems).mockResolvedValue(mockItems);
|
||||
|
||||
// Act
|
||||
@@ -136,8 +141,13 @@ describe('User Routes (/api/users)', () => {
|
||||
it('should add an item to the watchlist and return the new item', async () => {
|
||||
// Arrange
|
||||
const newItem = { itemName: 'Organic Bananas', category: 'Produce' };
|
||||
const mockAddedItem = { master_grocery_item_id: 99, name: 'Organic Bananas', created_at: new Date().toISOString() };
|
||||
// @ts-ignore
|
||||
const mockAddedItem: MasterGroceryItem = {
|
||||
master_grocery_item_id: 99,
|
||||
name: 'Organic Bananas',
|
||||
created_at: new Date().toISOString(),
|
||||
category_id: 1, // Add missing properties
|
||||
category_name: 'Produce'
|
||||
};
|
||||
vi.mocked(db.addWatchedItem).mockResolvedValue(mockAddedItem);
|
||||
|
||||
// Act
|
||||
@@ -167,8 +177,7 @@ describe('User Routes (/api/users)', () => {
|
||||
|
||||
describe('Shopping List Routes', () => {
|
||||
it('GET /shopping-lists should return all shopping lists for the user', async () => {
|
||||
const mockLists = [{ shopping_list_id: 1, user_id: mockUserProfile.user_id, name: 'Weekly Groceries', created_at: new Date().toISOString(), items: [] }];
|
||||
// @ts-ignore
|
||||
const mockLists: ShoppingList[] = [{ shopping_list_id: 1, user_id: mockUserProfile.user_id, name: 'Weekly Groceries', created_at: new Date().toISOString(), items: [] }];
|
||||
vi.mocked(db.getShoppingLists).mockResolvedValue(mockLists);
|
||||
|
||||
const response = await supertest(app).get('/api/users/shopping-lists');
|
||||
@@ -178,8 +187,7 @@ describe('User Routes (/api/users)', () => {
|
||||
});
|
||||
|
||||
it('POST /shopping-lists should create a new list', async () => {
|
||||
const mockNewList = { shopping_list_id: 2, user_id: mockUserProfile.user_id, name: 'Party Supplies', created_at: new Date().toISOString(), items: [] };
|
||||
// @ts-ignore
|
||||
const mockNewList: ShoppingList = { shopping_list_id: 2, user_id: mockUserProfile.user_id, name: 'Party Supplies', created_at: new Date().toISOString(), items: [] };
|
||||
vi.mocked(db.createShoppingList).mockResolvedValue(mockNewList);
|
||||
|
||||
const response = await supertest(app)
|
||||
@@ -201,8 +209,14 @@ describe('User Routes (/api/users)', () => {
|
||||
it('POST /shopping-lists/:listId/items should add an item to a list', async () => {
|
||||
const listId = 1;
|
||||
const itemData = { customItemName: 'Paper Towels' };
|
||||
const mockAddedItem = { shopping_list_item_id: 101, shopping_list_id: listId, quantity: 1, is_purchased: false, added_at: new Date().toISOString(), ...itemData };
|
||||
// @ts-ignore
|
||||
const mockAddedItem: ShoppingListItem = {
|
||||
shopping_list_item_id: 101,
|
||||
shopping_list_id: listId,
|
||||
quantity: 1,
|
||||
is_purchased: false,
|
||||
added_at: new Date().toISOString(),
|
||||
...itemData
|
||||
};
|
||||
vi.mocked(db.addShoppingListItem).mockResolvedValue(mockAddedItem);
|
||||
|
||||
const response = await supertest(app)
|
||||
@@ -216,8 +230,13 @@ describe('User Routes (/api/users)', () => {
|
||||
it('PUT /shopping-lists/items/:itemId should update an item', async () => {
|
||||
const itemId = 101;
|
||||
const updates = { is_purchased: true, quantity: 2 };
|
||||
const mockUpdatedItem = { shopping_list_item_id: itemId, shopping_list_id: 1, added_at: new Date().toISOString(), ...updates };
|
||||
// @ts-ignore
|
||||
const mockUpdatedItem: ShoppingListItem = {
|
||||
shopping_list_item_id: itemId,
|
||||
shopping_list_id: 1,
|
||||
added_at: new Date().toISOString(),
|
||||
custom_item_name: 'Item', // Add missing property
|
||||
...updates
|
||||
};
|
||||
vi.mocked(db.updateShoppingListItem).mockResolvedValue(mockUpdatedItem);
|
||||
|
||||
const response = await supertest(app)
|
||||
@@ -285,8 +304,12 @@ describe('User Routes (/api/users)', () => {
|
||||
describe('DELETE /account', () => {
|
||||
it('should delete the account with the correct password', async () => {
|
||||
// Arrange
|
||||
const userWithHash = { ...mockUserProfile.user, password_hash: 'hashed-password' };
|
||||
// @ts-ignore
|
||||
const userWithHash = {
|
||||
...mockUserProfile.user,
|
||||
password_hash: 'hashed-password',
|
||||
failed_login_attempts: 0, // Add missing properties
|
||||
last_failed_login: null
|
||||
};
|
||||
vi.mocked(db.findUserWithPasswordHashById).mockResolvedValue(userWithHash);
|
||||
vi.mocked(db.deleteUserById).mockResolvedValue(undefined);
|
||||
vi.mocked(bcrypt.compare).mockResolvedValue(true as never);
|
||||
@@ -302,8 +325,12 @@ describe('User Routes (/api/users)', () => {
|
||||
});
|
||||
|
||||
it('should return 403 for an incorrect password', async () => {
|
||||
const userWithHash = { ...mockUserProfile.user, password_hash: 'hashed-password' };
|
||||
// @ts-ignore
|
||||
const userWithHash = {
|
||||
...mockUserProfile.user,
|
||||
password_hash: 'hashed-password',
|
||||
failed_login_attempts: 0, // Add missing properties
|
||||
last_failed_login: null
|
||||
};
|
||||
vi.mocked(db.findUserWithPasswordHashById).mockResolvedValue(userWithHash);
|
||||
vi.mocked(bcrypt.compare).mockResolvedValue(false as never);
|
||||
|
||||
@@ -347,8 +374,7 @@ describe('User Routes (/api/users)', () => {
|
||||
|
||||
describe('GET and PUT /users/me/dietary-restrictions', () => {
|
||||
it('GET should return a list of restriction IDs', async () => {
|
||||
const mockRestrictions = [{ dietary_restriction_id: 1, name: 'Gluten-Free', type: 'diet' }];
|
||||
// @ts-ignore
|
||||
const mockRestrictions = [{ dietary_restriction_id: 1, name: 'Gluten-Free', type: 'diet' as const }];
|
||||
vi.mocked(db.getUserDietaryRestrictions).mockResolvedValue(mockRestrictions);
|
||||
|
||||
const response = await supertest(app).get('/api/users/me/dietary-restrictions');
|
||||
@@ -371,8 +397,7 @@ describe('User Routes (/api/users)', () => {
|
||||
|
||||
describe('GET and PUT /users/me/appliances', () => {
|
||||
it('GET should return a list of appliance IDs', async () => {
|
||||
const mockAppliances = [{ appliance_id: 2, name: 'Air Fryer' }];
|
||||
// @ts-ignore
|
||||
const mockAppliances: Appliance[] = [{ appliance_id: 2, name: 'Air Fryer' }];
|
||||
vi.mocked(db.getUserAppliances).mockResolvedValue(mockAppliances);
|
||||
|
||||
const response = await supertest(app).get('/api/users/me/appliances');
|
||||
@@ -382,8 +407,7 @@ describe('User Routes (/api/users)', () => {
|
||||
});
|
||||
|
||||
it('PUT should successfully set the appliances', async () => {
|
||||
// FIX: Passed [] instead of undefined to match expected return type UserAppliance[]
|
||||
// @ts-ignore
|
||||
// Pass an empty array to match the expected return type UserAppliance[]
|
||||
vi.mocked(db.setUserAppliances).mockResolvedValue([]);
|
||||
const applianceIds = [2, 4, 6];
|
||||
const response = await supertest(app).put('/api/users/me/appliances').send({ applianceIds });
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* It communicates with the application's own backend endpoints, which then securely
|
||||
* call the Google AI services. This ensures no API keys are exposed on the client.
|
||||
*/
|
||||
import type { FlyerItem } from "../types";
|
||||
import type { FlyerItem, Store } from "../types";
|
||||
import { logger } from "./logger";
|
||||
import { apiFetchWithAuth } from './apiClient';
|
||||
|
||||
@@ -109,14 +109,14 @@ export const searchWeb = async (items: Partial<FlyerItem>[], tokenOverride?: str
|
||||
* @param userLocation The user's current geographic coordinates.
|
||||
* @returns A text response with trip planning advice and a list of map sources.
|
||||
*/
|
||||
// export const planTripWithMaps = async (items: FlyerItem[], store: Store | undefined, userLocation: GeolocationCoordinates, tokenOverride?: string): Promise<Response> => {
|
||||
// logger.debug("Stub: planTripWithMaps called with location:", { userLocation });
|
||||
// return apiFetchWithAuth('/ai/plan-trip', {
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify({ items, store, userLocation }),
|
||||
// }, tokenOverride);
|
||||
// };
|
||||
export const planTripWithMaps = async (items: FlyerItem[], store: Store | undefined, userLocation: GeolocationCoordinates, tokenOverride?: string): Promise<Response> => {
|
||||
logger.debug("Stub: planTripWithMaps called with location:", { userLocation });
|
||||
return apiFetchWithAuth('/ai/plan-trip', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items, store, userLocation }),
|
||||
}, tokenOverride);
|
||||
};
|
||||
|
||||
/**
|
||||
* [STUB] Generates an image based on a text prompt using the Imagen model.
|
||||
|
||||
@@ -274,41 +274,41 @@ export const extractTextFromImageArea = async (
|
||||
* @param userLocation The user's current geographic coordinates.
|
||||
* @returns A text response with trip planning advice and a list of map sources.
|
||||
*/
|
||||
export const planTripWithMaps = async (): Promise<{text: string; sources: { uri: string; title: string; }[]}> => {
|
||||
// const topItems = items.slice(0, 5).map(i => i.item).join(', ');
|
||||
// const storeName = store?.name || 'the grocery store';
|
||||
export const planTripWithMaps = async (items: FlyerItem[], store: { name: string } | undefined, userLocation: GeolocationCoordinates): Promise<{text: string; sources: { uri: string; title: string; }[]}> => {
|
||||
const topItems = items.slice(0, 5).map(i => i.item).join(', ');
|
||||
const storeName = store?.name || 'the grocery store';
|
||||
|
||||
// try {
|
||||
// const response = await model.generateContent({
|
||||
// model: "gemini-2.5-flash",
|
||||
// // Make the prompt more specific by providing context for the location.
|
||||
// // This helps the AI ground its search more accurately.
|
||||
// contents: `My current location is latitude ${userLocation.latitude}, longitude ${userLocation.longitude}.
|
||||
// I have a shopping list with items like ${topItems}. Find the nearest ${storeName} to me and suggest the best route.
|
||||
// Also, are there any other specialty stores nearby (like a bakery or butcher) that might have good deals on related items?`,
|
||||
// config: {
|
||||
// tools: [{googleMaps: {}}],
|
||||
// toolConfig: {
|
||||
// retrievalConfig: {
|
||||
// latLng: {
|
||||
// latitude: userLocation.latitude,
|
||||
// longitude: userLocation.longitude
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
try {
|
||||
const response = await model.generateContent({
|
||||
model: "gemini-2.5-flash",
|
||||
// Make the prompt more specific by providing context for the location.
|
||||
// This helps the AI ground its search more accurately.
|
||||
contents: `My current location is latitude ${userLocation.latitude}, longitude ${userLocation.longitude}.
|
||||
I have a shopping list with items like ${topItems}. Find the nearest ${storeName} to me and suggest the best route.
|
||||
Also, are there any other specialty stores nearby (like a bakery or butcher) that might have good deals on related items?`,
|
||||
config: {
|
||||
tools: [{googleMaps: {}}],
|
||||
toolConfig: {
|
||||
retrievalConfig: {
|
||||
latLng: {
|
||||
latitude: userLocation.latitude,
|
||||
longitude: userLocation.longitude
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// // In a real implementation, you would render the map URLs from the sources.
|
||||
// const sources = (response.candidates?.[0]?.groundingMetadata?.groundingChunks || []).map(chunk => ({
|
||||
// uri: chunk.web?.uri || '',
|
||||
// title: chunk.web?.title || 'Untitled'
|
||||
// }));
|
||||
// return { text: response.text ?? '', sources };
|
||||
// } catch (apiError) {
|
||||
// logger.error("Google GenAI API call failed in planTripWithMaps:", { error: apiError });
|
||||
// throw apiError;
|
||||
// }
|
||||
// In a real implementation, you would render the map URLs from the sources.
|
||||
const sources = (response.candidates?.[0]?.groundingMetadata?.groundingChunks || []).map(chunk => ({
|
||||
uri: chunk.web?.uri || '',
|
||||
title: chunk.web?.title || 'Untitled'
|
||||
}));
|
||||
return { text: response.text ?? '', sources };
|
||||
} catch (apiError) {
|
||||
logger.error("Google GenAI API call failed in planTripWithMaps:", { error: apiError });
|
||||
throw apiError;
|
||||
}
|
||||
|
||||
// Return a 501 Not Implemented error as this feature is disabled.
|
||||
throw new Error("The 'planTripWithMaps' feature is currently disabled due to API costs.");
|
||||
|
||||
@@ -89,26 +89,26 @@ describe('AI API Routes Integration Tests', () => {
|
||||
expect(result).toEqual({ text: "The web says this is good.", sources: [] });
|
||||
});
|
||||
|
||||
// it('POST /api/ai/plan-trip should return a stubbed trip plan', async () => {
|
||||
// // The GeolocationCoordinates type requires more than just lat/lng.
|
||||
// // We create a complete mock object to satisfy the type.
|
||||
// const mockLocation: TestGeolocationCoordinates = {
|
||||
// latitude: 48.4284,
|
||||
// longitude: -123.3656,
|
||||
// accuracy: 100,
|
||||
// altitude: null,
|
||||
// altitudeAccuracy: null,
|
||||
// heading: null,
|
||||
// speed: null,
|
||||
// toJSON: () => ({}),
|
||||
// };
|
||||
// const response = await aiApiClient.planTripWithMaps([], undefined, mockLocation, authToken);
|
||||
// const result = await response.json();
|
||||
// expect(result).toBeDefined();
|
||||
// // Make the assertion less brittle. The AI might return "grocery store" (singular).
|
||||
// // Using a case-insensitive regex for "grocery store" is more robust.
|
||||
// expect(result.text).toMatch(/grocery store/i);
|
||||
// });
|
||||
it('POST /api/ai/plan-trip should return a stubbed trip plan', async () => {
|
||||
// The GeolocationCoordinates type requires more than just lat/lng.
|
||||
// We create a complete mock object to satisfy the type.
|
||||
const mockLocation: TestGeolocationCoordinates = {
|
||||
latitude: 48.4284,
|
||||
longitude: -123.3656,
|
||||
accuracy: 100,
|
||||
altitude: null,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: () => ({}),
|
||||
};
|
||||
const response = await aiApiClient.planTripWithMaps([], undefined, mockLocation, authToken);
|
||||
const result = await response.json();
|
||||
expect(result).toBeDefined();
|
||||
// The AI service is mocked in unit tests, but in integration it might be live.
|
||||
// For now, we just check that we get a text response.
|
||||
expect(result.text).toBeTypeOf('string');
|
||||
});
|
||||
|
||||
it('POST /api/ai/generate-image should reject because it is not implemented', async () => {
|
||||
// The backend for this is not stubbed and will throw an error.
|
||||
|
||||
Reference in New Issue
Block a user