many fixes resultnig from latest refactoring
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 5m46s

This commit is contained in:
2025-12-08 23:07:50 -08:00
parent e156c385a5
commit c1a032d5e6
23 changed files with 66 additions and 81 deletions

View File

@@ -20,7 +20,6 @@ import systemRouter from './src/routes/system.routes';
import healthRouter from './src/routes/health.routes';
import { errorHandler } from './src/middleware/errorHandler';
import { backgroundJobService, startBackgroundJobs } from './src/services/backgroundJobService.ts';
import * as db from './src/services/db/index.db';
import { analyticsQueue, weeklyAnalyticsQueue, gracefulShutdown } from './src/services/queueService.server';
// --- START DEBUG LOGGING ---

View File

@@ -1,12 +1,11 @@
// src/App.tsx
import React, { useState, useCallback, useEffect } from 'react';
import { Routes, Route, useParams, useLocation, Outlet } from 'react-router-dom';
import { Routes, Route, useParams, useLocation } from 'react-router-dom';
import { Toaster } from 'react-hot-toast';
import * as pdfjsLib from 'pdfjs-dist';
import { Header } from './components/Header';
import { logger } from './services/logger.client';
import type { Flyer, Profile, User, UserProfile } from './types';
import * as apiClient from './services/apiClient';
import type { Flyer, Profile, User } from './types';
import { ProfileManager } from './pages/admin/components/ProfileManager';
import { VoiceAssistant } from './features/voice-assistant/VoiceAssistant';
import { AdminPage } from './pages/admin/AdminPage';

View File

@@ -2,7 +2,7 @@
import React, { useState, useEffect } from 'react';
import * as apiClient from '../services/apiClient';
import { LeaderboardUser } from '../types';
import { logger } from '../services/logger';
import { logger } from '../services/logger.client';
import { Award, Crown, ShieldAlert } from 'lucide-react';
export const Leaderboard: React.FC = () => {

View File

@@ -8,7 +8,7 @@
import { Pool } from 'pg';
import bcrypt from 'bcrypt';
import { logger } from '../services/logger';
import { logger } from '../services/logger.server';
import { CATEGORIES } from '../types';
const pool = new Pool({

View File

@@ -8,7 +8,7 @@ import { TrashIcon } from '../../components/icons/TrashIcon';
import { SpeakerWaveIcon } from '../../components/icons/SpeakerWaveIcon';
import { generateSpeechFromText } from '../../services/aiApiClient';
import { decode, decodeAudioData } from '../../utils/audioUtils';
import { logger } from '../../services/logger';
import { logger } from '../../services/logger.client';
interface ShoppingListComponentProps {
user: User | null;

View File

@@ -9,7 +9,7 @@ import { CATEGORIES } from '../../types';
import { TrashIcon } from '../../components/icons/TrashIcon';
import { UserIcon } from '../../components/icons/UserIcon';
import { PlusCircleIcon } from '../../components/icons/PlusCircleIcon';
import { logger } from '../../services/logger';
import { logger } from '../../services/logger.client';
interface WatchedItemsListProps {
items: MasterGroceryItem[];
onAddItem: (itemName: string, category: string) => Promise<void>;

View File

@@ -5,7 +5,7 @@ import { MicrophoneIcon } from '../../components/icons/MicrophoneIcon';
// FIX: Corrected the import path. Types should be imported from the top-level '@google/genai' package.
import type { LiveServerMessage, Blob } from '@google/genai';
import { encode } from '../../utils/audioUtils';
import { logger } from '../../services/logger';
import { logger } from '../../services/logger.client';
import { XMarkIcon } from '../../components/icons/XMarkIcon';
interface VoiceAssistantProps {

View File

@@ -75,7 +75,7 @@ describe('useActiveDeals Hook', () => {
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify({ count: 0 })));
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify([])));
const { result } = renderHook(() => useActiveDeals(mockFlyers, mockWatchedItems));
renderHook(() => useActiveDeals(mockFlyers, mockWatchedItems));
await waitFor(() => {
// Only the valid flyer (id: 1) should be used in the API calls

View File

@@ -2,7 +2,7 @@
import React, { useState, useEffect } from 'react';
import { WatchedItemDeal } from '../types';
import { apiFetch } from '../services/apiClient';
import { logger } from '../services/logger';
import { logger } from '../services/logger.client';
import { AlertCircle, Tag, Store, Calendar } from 'lucide-react';
/**

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import * as apiClient from '../services/apiClient';
import { UserProfile, Achievement, UserAchievement } from '../types';
import { logger } from '../services/logger';
import { logger } from '../services/logger.client';
import { notifySuccess, notifyError } from '../services/notificationService';
import { AchievementsList } from '../components/AchievementsList';

View File

@@ -59,7 +59,7 @@ vi.mock('@bull-board/express', () => ({
}));
// Import the mocked modules to control them
import { BackgroundJobService, backgroundJobService } from '../services/backgroundJobService';
import { backgroundJobService } from '../services/backgroundJobService';
import { flyerQueue, analyticsQueue, cleanupQueue } from '../services/queueService.server';
// Mock the logger

View File

@@ -88,7 +88,7 @@ const createApp = (user?: UserProfile) => {
});
}
app.use('/api/admin', adminRouter);
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
app.use((err: Error, req: Request, res: Response ) => {
res.status(500).json({ message: err.message || 'Internal Server Error' });
});
return app;

View File

@@ -66,7 +66,7 @@ const createApp = (user?: UserProfile) => {
});
}
app.use('/api/admin', adminRouter);
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
app.use((err: Error, req: Request, res: Response) => {
res.status(500).json({ message: err.message || 'Internal Server Error' });
});
return app;

View File

@@ -4,7 +4,7 @@ import supertest from 'supertest';
import express, { Request, Response, NextFunction } from 'express';
import adminRouter from './admin.routes';
import { createMockUserProfile } from '../tests/utils/mockFactories';
import { User, UserProfile, SuggestedCorrection, Brand, Recipe, RecipeComment, UnmatchedFlyerItem, ActivityLogItem, AdminUserView } from '../types';
import { User, UserProfile } from '../types';
// Mock the Service Layer directly.
// The admin.routes.ts file imports from '.../db/index.db'. We need to mock that module.
@@ -90,7 +90,7 @@ const createApp = (user?: UserProfile) => {
});
}
app.use('/api/admin', adminRouter);
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
app.use((err: Error, req: Request, res: Response) => {
res.status(500).json({ message: err.message || 'Internal Server Error' });
});
return app;

View File

@@ -1,7 +1,7 @@
// src/routes/ai.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import express, { type Request, type Response, type NextFunction, Router } from 'express';
import express, { type Request, type Response, type NextFunction } from 'express';
import path from 'node:path';
import aiRouter from './ai.routes';
import { createMockUserProfile, createMockFlyer } from '../tests/utils/mockFactories';
@@ -61,7 +61,7 @@ app.use(express.json({ strict: false }));
app.use('/api/ai', aiRouter);
// Add a generic error handler to catch errors passed via next()
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
app.use((err: Error, req: Request, res: Response) => {
res.status(500).json({ message: err.message || 'Internal Server Error' });
});

View File

@@ -276,7 +276,7 @@ router.post('/flyers/process', optionalAuth, uploadToDisk.single('flyerImage'),
// 3. Create flyer and its items in a transaction
const { flyer: newFlyer, items: newItems } = await createFlyerAndItems(flyerData, itemsArray);
logger.info(`Successfully processed and saved new flyer: ${newFlyer.file_name} (ID: ${newFlyer.flyer_id})`);
logger.info(`Successfully processed and saved new flyer: ${newFlyer.file_name} (ID: ${newFlyer.flyer_id}) with ${newItems.length} items.`);
// Log this significant event
await db.adminRepo.logActivity({

View File

@@ -54,7 +54,7 @@ type PassportCallback = (error: Error | null, user: Express.User | false, info?:
// Mock Passport middleware
vi.mock('./passport.routes', () => ({
default: {
authenticate: (strategy: string, options: Record<string, unknown>, callback: PassportCallback) => (req: Request, res: any, next: any) => {
authenticate: (strategy: string, options: Record<string, unknown>, callback: PassportCallback) => (req: Request, res: any) => {
// Logic to simulate passport authentication outcome based on test input
if (req.body.password === 'wrong_password') {
// Simulate incorrect credentials
@@ -84,7 +84,7 @@ app.use(cookieParser()); // Add cookie-parser middleware to populate req.cookies
app.use('/api/auth', authRouter);
// Add error handler to catch and log 500s during tests
app.use((err: any, req: Request, res: any, next: any) => {
app.use((err: any, req: Request, res: any) => {
console.error('[TEST APP ERROR]', err);
res.status(500).json({ message: err.message });
});

View File

@@ -76,7 +76,7 @@ router.post('/register', async (req, res, next) => {
newUser = await repoWithTransaction.createUser(email, hashedPassword, { full_name, avatar_url });
await client.query('COMMIT');
} catch (error: any) {
} catch (error: unknown) {
if (error instanceof UniqueConstraintError) {
// If the email is a duplicate, return a 409 Conflict status.
return res.status(409).json({ message: error.message });

View File

@@ -45,7 +45,6 @@ fs.mkdir(avatarUploadDir, { recursive: true }).catch(err => {
const avatarStorage = multer.diskStorage({
destination: (req, file, cb) => cb(null, avatarUploadDir),
filename: (req, file, cb) => {
const user = req.user as User;
const uniqueSuffix = `${(req.user as UserProfile).user.user_id}-${Date.now()}${path.extname(file.originalname)}`;
cb(null, uniqueSuffix);
},

View File

@@ -1,5 +1,5 @@
// src/services/aiService.server.test.ts
import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { MasterGroceryItem } from '../types';
// Import the class, not the singleton instance, so we can instantiate it with mocks.
import { AIService } from './aiService.server';

View File

@@ -1,5 +1,5 @@
// src/services/apiClient.test.ts
import { describe, it, expect, vi, beforeAll, afterAll, afterEach, beforeEach } from 'vitest';
import { describe, it, expect, vi, afterAll, afterEach, beforeEach } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
@@ -43,27 +43,31 @@ describe('API Client', () => {
// These variables will be used to capture details from the fetch call.
let capturedUrl: URL | null = null;
let capturedHeaders: Headers | null = null;
let capturedBody: any = null;
let capturedBody: string | FormData | Record<string, unknown> | null = null;
beforeEach(() => {
// This mock now captures the arguments passed to fetch before returning a successful response.
global.fetch = vi.fn((url, options) => {
// Define a correctly typed mock function for fetch.
const mockFetch = (url: RequestInfo | URL, options?: RequestInit): Promise<Response> => {
capturedUrl = new URL(url as string, 'http://localhost');
capturedHeaders = options?.headers as Headers;
// FIX: Parse body if it is a string
if (typeof options?.body === 'string') {
try {
capturedBody = JSON.parse(options.body);
} catch {
capturedBody = options.body;
}
} else if (options?.body instanceof FormData) {
capturedBody = options.body;
} else {
capturedBody = options?.body || null;
capturedBody = null;
}
return Promise.resolve(new Response(JSON.stringify({ data: 'mock-success' }), { status: 200, headers: new Headers() }));
}) as any;
return Promise.resolve(new Response(JSON.stringify({ data: 'mock-success' }), { status: 200, headers: new Headers() as Headers }));
};
// Assign the mock function to global.fetch and cast it to a Vitest mock.
global.fetch = vi.fn(mockFetch);
});
afterEach(() => {
@@ -184,10 +188,8 @@ describe('API Client', () => {
describe('Budget API Functions', () => {
it('getBudgets should call the correct endpoint', async () => {
let wasCalled = false;
server.use(
http.get('http://localhost/api/budgets', () => {
wasCalled = true;
return HttpResponse.json([]);
})
);
@@ -350,10 +352,8 @@ describe('API Client', () => {
it('forkRecipe should send a POST request to the correct URL', async () => {
const recipeId = 99;
let wasCalled = false;
server.use(
http.post(`http://localhost/api/recipes/${recipeId}/fork`, () => {
wasCalled = true;
return HttpResponse.json({ success: true });
})
);
@@ -459,10 +459,8 @@ describe('API Client', () => {
});
it('checkDbSchema should call the correct health check endpoint', async () => {
let wasCalled = false;
server.use(
http.get('http://localhost/api/health/db-schema', () => {
wasCalled = true;
return HttpResponse.json({ success: true });
})
);
@@ -471,10 +469,8 @@ describe('API Client', () => {
});
it('checkStorage should call the correct health check endpoint', async () => {
let wasCalled = false;
server.use(
http.get('http://localhost/api/health/storage', () => {
wasCalled = true;
return HttpResponse.json({ success: true });
})
);
@@ -483,10 +479,8 @@ describe('API Client', () => {
});
it('checkDbPoolHealth should call the correct health check endpoint', async () => {
let wasCalled = false;
server.use(
http.get('http://localhost/api/health/db-pool', () => {
wasCalled = true;
return HttpResponse.json({ success: true });
})
);
@@ -495,10 +489,8 @@ describe('API Client', () => {
});
it('checkRedisHealth should call the correct health check endpoint', async () => {
let wasCalled = false;
server.use(
http.get('http://localhost/api/health/redis', () => {
wasCalled = true;
return HttpResponse.json({ success: true });
})
);
@@ -507,10 +499,8 @@ describe('API Client', () => {
});
it('checkPm2Status should call the correct system endpoint', async () => {
let wasCalled = false;
server.use(
http.get('http://localhost/api/system/pm2-status', () => {
wasCalled = true;
return HttpResponse.json({ success: true });
})
);
@@ -519,10 +509,8 @@ describe('API Client', () => {
});
it('fetchFlyers should call the correct public endpoint', async () => {
let wasCalled = false;
server.use(
http.get('http://localhost/api/flyers', () => {
wasCalled = true;
return HttpResponse.json([]);
})
);
@@ -531,10 +519,8 @@ describe('API Client', () => {
});
it('fetchMasterItems should call the correct public endpoint', async () => {
let wasCalled = false;
server.use(
http.get('http://localhost/api/master-items', () => {
wasCalled = true;
return HttpResponse.json([]);
})
);
@@ -543,10 +529,8 @@ describe('API Client', () => {
});
it('fetchCategories should call the correct public endpoint', async () => {
let wasCalled = false;
server.use(
http.get('http://localhost/api/categories', () => {
wasCalled = true;
return HttpResponse.json([]);
})
);
@@ -556,10 +540,8 @@ describe('API Client', () => {
it('fetchFlyerItems should call the correct public endpoint for a specific flyer', async () => {
const flyerId = 123;
let wasCalled = false;
server.use(
http.get(`http://localhost/api/flyers/${flyerId}/items`, () => {
wasCalled = true;
return HttpResponse.json([]);
})
);

View File

@@ -9,15 +9,16 @@ vi.mock('node-cron', () => ({ default: { schedule: mockCronSchedule } }));
import { BackgroundJobService, startBackgroundJobs } from './backgroundJobService';
import type { Queue } from 'bullmq';
import { WatchedItemDeal } from '../types';
import { AdminUserView } from './db/admin.db';
describe('Background Job Service', () => {
// Create mock dependencies that will be injected into the service
const mockDbService = {
createBulkNotifications: vi.fn(),
const mockPersonalizationRepo = {
getBestSalePricesForAllUsers: vi.fn(),
};
const mockNotificationRepo = {
createBulkNotifications: vi.fn(),
};
const mockEmailQueue = {
add: vi.fn(),
};
@@ -47,27 +48,27 @@ describe('Background Job Service', () => {
];
// Instantiate the service with mock dependencies for each test run
const service = new BackgroundJobService(mockDbService as any, mockEmailQueue as unknown as Queue<any>, mockLogger);
const service = new BackgroundJobService(mockPersonalizationRepo as any, mockNotificationRepo as any, mockEmailQueue as unknown as Queue<any>, mockLogger);
it('should do nothing if no deals are found for any user', async () => {
mockDbService.getBestSalePricesForAllUsers.mockResolvedValue([]);
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue([]);
await service.runDailyDealCheck();
expect(mockLogger.info).toHaveBeenCalledWith('[BackgroundJob] Starting daily deal check for all users...');
expect(mockLogger.info).toHaveBeenCalledWith('[BackgroundJob] No deals found for any watched items. Skipping.');
expect(mockDbService.getBestSalePricesForAllUsers).toHaveBeenCalledTimes(1);
expect(mockPersonalizationRepo.getBestSalePricesForAllUsers).toHaveBeenCalledTimes(1);
expect(mockEmailQueue.add).not.toHaveBeenCalled();
expect(mockDbService.createBulkNotifications).not.toHaveBeenCalled();
expect(mockNotificationRepo.createBulkNotifications).not.toHaveBeenCalled();
});
it('should create notifications and enqueue emails when deals are found', async () => {
mockDbService.getBestSalePricesForAllUsers.mockResolvedValue(mockDealsForAllUsers);
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue(mockDealsForAllUsers);
await service.runDailyDealCheck();
// Check that it fetched all deals once
expect(mockDbService.getBestSalePricesForAllUsers).toHaveBeenCalledTimes(1);
expect(mockPersonalizationRepo.getBestSalePricesForAllUsers).toHaveBeenCalledTimes(1);
// Check that email jobs were enqueued for both users
expect(mockEmailQueue.add).toHaveBeenCalledTimes(2);
@@ -80,8 +81,8 @@ describe('Background Job Service', () => {
expect(secondEmailJob[1].html).toContain('Bread');
// Check that in-app notifications were created for both users
expect(mockDbService.createBulkNotifications).toHaveBeenCalledTimes(1);
const notificationPayload = mockDbService.createBulkNotifications.mock.calls[0][0];
expect(mockNotificationRepo.createBulkNotifications).toHaveBeenCalledTimes(1);
const notificationPayload = mockNotificationRepo.createBulkNotifications.mock.calls[0][0];
expect(notificationPayload).toHaveLength(2);
// Use expect.arrayContaining to be order-agnostic.
expect(notificationPayload).toEqual(expect.arrayContaining([
@@ -99,7 +100,7 @@ describe('Background Job Service', () => {
});
it('should handle and log errors for individual users without stopping the process', async () => {
mockDbService.getBestSalePricesForAllUsers.mockResolvedValue(mockDealsForAllUsers);
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockResolvedValue(mockDealsForAllUsers);
// Simulate the email queue failing for the first user but succeeding for the second
mockEmailQueue.add
.mockRejectedValueOnce(new Error('Email queue is down'))
@@ -116,13 +117,13 @@ describe('Background Job Service', () => {
// Check that it still processed user 2 successfully
// The email queue add should be attempted for both users.
expect(mockEmailQueue.add).toHaveBeenCalledTimes(2);
expect(mockDbService.createBulkNotifications).toHaveBeenCalledTimes(1);
expect(mockDbService.createBulkNotifications.mock.calls[0][0]).toHaveLength(1); // Only one notification created
expect(mockDbService.createBulkNotifications.mock.calls[0][0][0].user_id).toBe('user-2');
expect(mockNotificationRepo.createBulkNotifications).toHaveBeenCalledTimes(1);
expect(mockNotificationRepo.createBulkNotifications.mock.calls[0][0]).toHaveLength(1); // Only one notification created
expect(mockNotificationRepo.createBulkNotifications.mock.calls[0][0][0].user_id).toBe('user-2');
});
it('should log a critical error if getBestSalePricesForAllUsers fails', async () => {
mockDbService.getBestSalePricesForAllUsers.mockRejectedValue(new Error('Critical DB Failure'));
mockPersonalizationRepo.getBestSalePricesForAllUsers.mockRejectedValue(new Error('Critical DB Failure'));
await expect(service.runDailyDealCheck()).rejects.toThrow('Critical DB Failure');
expect(mockLogger.error).toHaveBeenCalledWith(
'[BackgroundJob] A critical error occurred during the daily deal check:',
@@ -139,23 +140,28 @@ describe('Background Job Service', () => {
const mockAnalyticsQueue = {
add: vi.fn(),
} as unknown as Queue;
const mockWeeklyAnalyticsQueue = {
add: vi.fn(),
} as unknown as Queue;
beforeEach(() => {
mockCronSchedule.mockClear();
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockClear();
vi.mocked(mockAnalyticsQueue.add).mockClear();
vi.mocked(mockWeeklyAnalyticsQueue.add).mockClear();
});
it('should schedule two cron jobs with the correct schedules', () => {
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue);
it('should schedule three cron jobs with the correct schedules', () => {
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
expect(mockCronSchedule).toHaveBeenCalledTimes(2);
expect(mockCronSchedule).toHaveBeenCalledTimes(3);
expect(mockCronSchedule).toHaveBeenCalledWith('0 2 * * *', expect.any(Function));
expect(mockCronSchedule).toHaveBeenCalledWith('0 3 * * *', expect.any(Function));
expect(mockCronSchedule).toHaveBeenCalledWith('0 4 * * 0', expect.any(Function));
});
it('should call runDailyDealCheck when the first cron job function is executed', async () => {
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
// Get the callback function for the first cron job
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
@@ -167,7 +173,7 @@ describe('Background Job Service', () => {
it('should log an error and release the lock if runDailyDealCheck fails', async () => {
const jobError = new Error('Cron job failed');
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockRejectedValue(jobError);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
await dailyDealCheckCallback();
@@ -189,7 +195,7 @@ describe('Background Job Service', () => {
// Make the first call hang indefinitely
vi.mocked(mockBackgroundJobService.runDailyDealCheck).mockReturnValue(new Promise(() => {}));
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
const dailyDealCheckCallback = mockCronSchedule.mock.calls[0][1];
// Trigger the job once, it will hang
@@ -204,7 +210,7 @@ describe('Background Job Service', () => {
});
it('should enqueue an analytics job when the second cron job function is executed', async () => {
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
await analyticsJobCallback();
@@ -215,7 +221,7 @@ describe('Background Job Service', () => {
it('should log an error if enqueuing the analytics job fails', async () => {
const queueError = new Error('Redis is down');
vi.mocked(mockAnalyticsQueue.add).mockRejectedValue(queueError);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue);
startBackgroundJobs(mockBackgroundJobService, mockAnalyticsQueue, mockWeeklyAnalyticsQueue);
const analyticsJobCallback = mockCronSchedule.mock.calls[1][1];
await analyticsJobCallback();

View File

@@ -1,5 +1,5 @@
// src/services/geocodingService.server.test.ts
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// --- Hoisted Mocks ---
const mocks = vi.hoisted(() => ({