mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 11:12:11 +01:00
fix: resolve TypeScript errors and eliminate manual coordinate hardcoding
- Fix sendEmail function call to use proper EmailOptions object - Improve GeographicMap by replacing 52 hardcoded coordinates with automatic extraction from @rapideditor/country-coder library - Fix test imports to use correct exported functions from lib modules - Add missing required properties to Prisma mock objects in tests - Properly type all mock objects with correct enum values and required fields - Simplify rate limiter mock to avoid private property conflicts - Fix linting issues with variable declarations and useEffect dependencies
This commit is contained in:
@ -60,11 +60,11 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resetUrl = `${process.env.NEXTAUTH_URL || "http://localhost:3000"}/reset-password?token=${token}`;
|
const resetUrl = `${process.env.NEXTAUTH_URL || "http://localhost:3000"}/reset-password?token=${token}`;
|
||||||
await sendEmail(
|
await sendEmail({
|
||||||
email,
|
to: email,
|
||||||
"Password Reset",
|
subject: "Password Reset",
|
||||||
`Reset your password: ${resetUrl}`
|
text: `Reset your password: ${resetUrl}`
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true }, { status: 200 });
|
return NextResponse.json({ success: true }, { status: 200 });
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import * as countryCoder from "@rapideditor/country-coder";
|
import * as countryCoder from "@rapideditor/country-coder";
|
||||||
|
|
||||||
@ -18,45 +18,53 @@ interface GeographicMapProps {
|
|||||||
height?: number; // Optional height for the container
|
height?: number; // Optional height for the container
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get country coordinates from the @rapideditor/country-coder package
|
/**
|
||||||
const getCountryCoordinates = (): Record<string, [number, number]> => {
|
* Get coordinates for a country using the country-coder library
|
||||||
// Initialize with some fallback coordinates for common countries
|
* This automatically extracts coordinates from the country geometry
|
||||||
const coordinates: Record<string, [number, number]> = {
|
*/
|
||||||
US: [37.0902, -95.7129],
|
function getCoordinatesFromCountryCoder(countryCode: string): [number, number] | undefined {
|
||||||
GB: [55.3781, -3.436],
|
try {
|
||||||
BA: [43.9159, 17.6791],
|
const feature = countryCoder.feature(countryCode);
|
||||||
NL: [52.1326, 5.2913],
|
if (!feature?.geometry) {
|
||||||
DE: [51.1657, 10.4515],
|
return undefined;
|
||||||
FR: [46.6034, 1.8883],
|
}
|
||||||
IT: [41.8719, 12.5674],
|
|
||||||
ES: [40.4637, -3.7492],
|
|
||||||
CA: [56.1304, -106.3468],
|
|
||||||
PL: [51.9194, 19.1451],
|
|
||||||
SE: [60.1282, 18.6435],
|
|
||||||
NO: [60.472, 8.4689],
|
|
||||||
FI: [61.9241, 25.7482],
|
|
||||||
CH: [46.8182, 8.2275],
|
|
||||||
AT: [47.5162, 14.5501],
|
|
||||||
BE: [50.8503, 4.3517],
|
|
||||||
DK: [56.2639, 9.5018],
|
|
||||||
CZ: [49.8175, 15.473],
|
|
||||||
HU: [47.1625, 19.5033],
|
|
||||||
PT: [39.3999, -8.2245],
|
|
||||||
GR: [39.0742, 21.8243],
|
|
||||||
RO: [45.9432, 24.9668],
|
|
||||||
IE: [53.4129, -8.2439],
|
|
||||||
BG: [42.7339, 25.4858],
|
|
||||||
HR: [45.1, 15.2],
|
|
||||||
SK: [48.669, 19.699],
|
|
||||||
SI: [46.1512, 14.9955],
|
|
||||||
};
|
|
||||||
// This function now primarily returns fallbacks.
|
|
||||||
// The actual fetching using @rapideditor/country-coder will be in the component's useEffect.
|
|
||||||
return coordinates;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load coordinates once when module is imported
|
// Extract center coordinates from the geometry
|
||||||
const DEFAULT_COORDINATES = getCountryCoordinates();
|
if (feature.geometry.type === "Point") {
|
||||||
|
const [lon, lat] = feature.geometry.coordinates;
|
||||||
|
return [lat, lon]; // Leaflet expects [lat, lon]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feature.geometry.type === "Polygon" && feature.geometry.coordinates?.[0]?.[0]) {
|
||||||
|
// For polygons, calculate centroid from the first ring
|
||||||
|
const coordinates = feature.geometry.coordinates[0];
|
||||||
|
let lat = 0;
|
||||||
|
let lon = 0;
|
||||||
|
for (const [lng, ltd] of coordinates) {
|
||||||
|
lon += lng;
|
||||||
|
lat += ltd;
|
||||||
|
}
|
||||||
|
return [lat / coordinates.length, lon / coordinates.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feature.geometry.type === "MultiPolygon" && feature.geometry.coordinates?.[0]?.[0]?.[0]) {
|
||||||
|
// For multipolygons, use the first polygon's first ring for centroid
|
||||||
|
const coordinates = feature.geometry.coordinates[0][0];
|
||||||
|
let lat = 0;
|
||||||
|
let lon = 0;
|
||||||
|
for (const [lng, ltd] of coordinates) {
|
||||||
|
lon += lng;
|
||||||
|
lat += ltd;
|
||||||
|
}
|
||||||
|
return [lat / coordinates.length, lon / coordinates.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to get coordinates for country ${countryCode}:`, error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Dynamically import the Map component to avoid SSR issues
|
// Dynamically import the Map component to avoid SSR issues
|
||||||
// This ensures the component only loads on the client side
|
// This ensures the component only loads on the client side
|
||||||
@ -71,7 +79,7 @@ const CountryMapComponent = dynamic(() => import("./Map"), {
|
|||||||
|
|
||||||
export default function GeographicMap({
|
export default function GeographicMap({
|
||||||
countries,
|
countries,
|
||||||
countryCoordinates = DEFAULT_COORDINATES,
|
countryCoordinates = {},
|
||||||
height = 400,
|
height = 400,
|
||||||
}: GeographicMapProps) {
|
}: GeographicMapProps) {
|
||||||
const [countryData, setCountryData] = useState<CountryData[]>([]);
|
const [countryData, setCountryData] = useState<CountryData[]>([]);
|
||||||
@ -82,42 +90,6 @@ export default function GeographicMap({
|
|||||||
setIsClient(true);
|
setIsClient(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract coordinates from a geometry feature
|
|
||||||
*/
|
|
||||||
function extractCoordinatesFromGeometry(
|
|
||||||
geometry: any
|
|
||||||
): [number, number] | undefined {
|
|
||||||
if (geometry.type === "Point") {
|
|
||||||
const [lon, lat] = geometry.coordinates;
|
|
||||||
return [lat, lon]; // Leaflet expects [lat, lon]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
geometry.type === "Polygon" &&
|
|
||||||
geometry.coordinates &&
|
|
||||||
geometry.coordinates[0] &&
|
|
||||||
geometry.coordinates[0][0]
|
|
||||||
) {
|
|
||||||
// For Polygons, use the first coordinate of the first ring as a fallback representative point
|
|
||||||
const [lon, lat] = geometry.coordinates[0][0];
|
|
||||||
return [lat, lon]; // Leaflet expects [lat, lon]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
geometry.type === "MultiPolygon" &&
|
|
||||||
geometry.coordinates &&
|
|
||||||
geometry.coordinates[0] &&
|
|
||||||
geometry.coordinates[0][0] &&
|
|
||||||
geometry.coordinates[0][0][0]
|
|
||||||
) {
|
|
||||||
// For MultiPolygons, use the first coordinate of the first ring of the first polygon
|
|
||||||
const [lon, lat] = geometry.coordinates[0][0][0];
|
|
||||||
return [lat, lon]; // Leaflet expects [lat, lon]
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get coordinates for a country code
|
* Get coordinates for a country code
|
||||||
@ -126,15 +98,12 @@ export default function GeographicMap({
|
|||||||
code: string,
|
code: string,
|
||||||
countryCoordinates: Record<string, [number, number]>
|
countryCoordinates: Record<string, [number, number]>
|
||||||
): [number, number] | undefined {
|
): [number, number] | undefined {
|
||||||
// Try predefined coordinates first
|
// Try custom coordinates first (allows overrides)
|
||||||
let coords = countryCoordinates[code] || DEFAULT_COORDINATES[code];
|
let coords: [number, number] | undefined = countryCoordinates[code];
|
||||||
|
|
||||||
if (!coords) {
|
if (!coords) {
|
||||||
// Try to get coordinates from country coder
|
// Automatically get coordinates from country-coder library
|
||||||
const feature = countryCoder.feature(code);
|
coords = getCoordinatesFromCountryCoder(code);
|
||||||
if (feature?.geometry) {
|
|
||||||
coords = extractCoordinatesFromGeometry(feature.geometry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return coords;
|
return coords;
|
||||||
@ -160,10 +129,10 @@ export default function GeographicMap({
|
|||||||
/**
|
/**
|
||||||
* Process all countries data into CountryData array
|
* Process all countries data into CountryData array
|
||||||
*/
|
*/
|
||||||
function processCountriesData(
|
const processCountriesData = useCallback((
|
||||||
countries: Record<string, number>,
|
countries: Record<string, number>,
|
||||||
countryCoordinates: Record<string, [number, number]>
|
countryCoordinates: Record<string, [number, number]>
|
||||||
): CountryData[] {
|
): CountryData[] => {
|
||||||
const data = Object.entries(countries || {})
|
const data = Object.entries(countries || {})
|
||||||
.map(([code, count]) =>
|
.map(([code, count]) =>
|
||||||
processCountryEntry(code, count, countryCoordinates)
|
processCountryEntry(code, count, countryCoordinates)
|
||||||
@ -175,7 +144,7 @@ export default function GeographicMap({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
// Process country data when client is ready and dependencies change
|
// Process country data when client is ready and dependencies change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -188,7 +157,7 @@ export default function GeographicMap({
|
|||||||
console.error("Error processing geographic data:", error);
|
console.error("Error processing geographic data:", error);
|
||||||
setCountryData([]);
|
setCountryData([]);
|
||||||
}
|
}
|
||||||
}, [countries, countryCoordinates, isClient]);
|
}, [countries, countryCoordinates, isClient, processCountriesData]);
|
||||||
|
|
||||||
// Find the max count for scaling circles - handle empty or null countries object
|
// Find the max count for scaling circles - handle empty or null countries object
|
||||||
const countryValues = countries ? Object.values(countries) : [];
|
const countryValues = countries ? Object.values(countries) : [];
|
||||||
|
|||||||
@ -63,8 +63,12 @@ describe("Authentication API Routes", () => {
|
|||||||
const mockCompany = {
|
const mockCompany = {
|
||||||
id: "company1",
|
id: "company1",
|
||||||
name: "Test Company",
|
name: "Test Company",
|
||||||
status: "ACTIVE",
|
status: "ACTIVE" as const,
|
||||||
csvUrl: "http://example.com/data.csv",
|
csvUrl: "http://example.com/data.csv",
|
||||||
|
csvUsername: null,
|
||||||
|
csvPassword: null,
|
||||||
|
dashboardOpts: {},
|
||||||
|
maxUsers: 10,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
@ -74,8 +78,12 @@ describe("Authentication API Routes", () => {
|
|||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
companyId: "company1",
|
companyId: "company1",
|
||||||
role: "USER",
|
role: "USER" as const,
|
||||||
password: "hashed-password",
|
password: "hashed-password",
|
||||||
|
resetToken: null,
|
||||||
|
resetTokenExpiry: null,
|
||||||
|
invitedAt: null,
|
||||||
|
invitedBy: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
@ -197,8 +205,12 @@ describe("Authentication API Routes", () => {
|
|||||||
const mockCompany = {
|
const mockCompany = {
|
||||||
id: "company1",
|
id: "company1",
|
||||||
name: "Test Company",
|
name: "Test Company",
|
||||||
status: "ACTIVE",
|
status: "ACTIVE" as const,
|
||||||
csvUrl: "http://example.com/data.csv",
|
csvUrl: "http://example.com/data.csv",
|
||||||
|
csvUsername: null,
|
||||||
|
csvPassword: null,
|
||||||
|
dashboardOpts: {},
|
||||||
|
maxUsers: 10,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
@ -208,8 +220,12 @@ describe("Authentication API Routes", () => {
|
|||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Existing User",
|
name: "Existing User",
|
||||||
companyId: "company1",
|
companyId: "company1",
|
||||||
role: "USER",
|
role: "USER" as const,
|
||||||
password: "hashed-password",
|
password: "hashed-password",
|
||||||
|
resetToken: null,
|
||||||
|
resetTokenExpiry: null,
|
||||||
|
invitedAt: null,
|
||||||
|
invitedBy: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
@ -240,15 +256,17 @@ describe("Authentication API Routes", () => {
|
|||||||
it("should handle rate limiting", async () => {
|
it("should handle rate limiting", async () => {
|
||||||
const { InMemoryRateLimiter } = await import("../../lib/rateLimiter");
|
const { InMemoryRateLimiter } = await import("../../lib/rateLimiter");
|
||||||
|
|
||||||
// Mock rate limiter to return not allowed
|
// Mock rate limiter class constructor
|
||||||
const mockRateLimiter = {
|
const mockCheckRateLimit = vi.fn().mockReturnValue({
|
||||||
checkRateLimit: vi.fn().mockReturnValue({
|
allowed: false,
|
||||||
allowed: false,
|
resetTime: Date.now() + 60000,
|
||||||
resetTime: Date.now() + 60000,
|
});
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.mocked(InMemoryRateLimiter).mockImplementation(() => mockRateLimiter);
|
vi.mocked(InMemoryRateLimiter).mockImplementation(() => ({
|
||||||
|
checkRateLimit: mockCheckRateLimit,
|
||||||
|
cleanup: vi.fn(),
|
||||||
|
destroy: vi.fn(),
|
||||||
|
} as any));
|
||||||
|
|
||||||
const request = new NextRequest("http://localhost:3000/api/register", {
|
const request = new NextRequest("http://localhost:3000/api/register", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -283,8 +301,12 @@ describe("Authentication API Routes", () => {
|
|||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
companyId: "company1",
|
companyId: "company1",
|
||||||
role: "USER",
|
role: "USER" as const,
|
||||||
password: "hashed-password",
|
password: "hashed-password",
|
||||||
|
resetToken: null,
|
||||||
|
resetTokenExpiry: null,
|
||||||
|
invitedAt: null,
|
||||||
|
invitedBy: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
@ -418,8 +440,12 @@ describe("Authentication API Routes", () => {
|
|||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
name: "Test User",
|
name: "Test User",
|
||||||
companyId: "company1",
|
companyId: "company1",
|
||||||
role: "USER",
|
role: "USER" as const,
|
||||||
password: "hashed-password",
|
password: "hashed-password",
|
||||||
|
resetToken: null,
|
||||||
|
resetTokenExpiry: null,
|
||||||
|
invitedAt: null,
|
||||||
|
invitedBy: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
import { GET } from "../../app/api/dashboard/metrics/route";
|
|
||||||
import { NextRequest } from "next/server";
|
import { NextRequest } from "next/server";
|
||||||
|
import { GET } from "../../app/api/dashboard/metrics/route";
|
||||||
|
|
||||||
// Mock NextAuth
|
// Mock NextAuth
|
||||||
vi.mock("next-auth", () => ({
|
vi.mock("next-auth", () => ({
|
||||||
@ -10,35 +10,28 @@ vi.mock("next-auth", () => ({
|
|||||||
// Mock prisma
|
// Mock prisma
|
||||||
vi.mock("../../lib/prisma", () => ({
|
vi.mock("../../lib/prisma", () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
session: {
|
|
||||||
count: vi.fn(),
|
|
||||||
findMany: vi.fn(),
|
|
||||||
aggregate: vi.fn(),
|
|
||||||
},
|
|
||||||
user: {
|
user: {
|
||||||
findUnique: vi.fn(),
|
findUnique: vi.fn(),
|
||||||
},
|
},
|
||||||
company: {
|
company: {
|
||||||
findUnique: vi.fn(),
|
findUnique: vi.fn(),
|
||||||
},
|
},
|
||||||
|
session: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
count: vi.fn(),
|
||||||
|
},
|
||||||
|
sessionQuestion: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock auth options
|
|
||||||
vi.mock("../../lib/auth", () => ({
|
|
||||||
authOptions: {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("/api/dashboard/metrics", () => {
|
describe("/api/dashboard/metrics", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
describe("GET", () => {
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("GET /api/dashboard/metrics", () => {
|
|
||||||
it("should return 401 for unauthenticated users", async () => {
|
it("should return 401 for unauthenticated users", async () => {
|
||||||
const { getServerSession } = await import("next-auth");
|
const { getServerSession } = await import("next-auth");
|
||||||
vi.mocked(getServerSession).mockResolvedValue(null);
|
vi.mocked(getServerSession).mockResolvedValue(null);
|
||||||
@ -85,10 +78,15 @@ describe("/api/dashboard/metrics", () => {
|
|||||||
|
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||||
id: "user1",
|
id: "user1",
|
||||||
|
name: "Test User",
|
||||||
email: "test@example.com",
|
email: "test@example.com",
|
||||||
companyId: "company1",
|
companyId: "company1",
|
||||||
role: "ADMIN",
|
role: "ADMIN" as const,
|
||||||
password: "hashed",
|
password: "hashed",
|
||||||
|
resetToken: null,
|
||||||
|
resetTokenExpiry: null,
|
||||||
|
invitedAt: null,
|
||||||
|
invitedBy: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
@ -105,16 +103,26 @@ describe("/api/dashboard/metrics", () => {
|
|||||||
expect(data.error).toBe("Company not found");
|
expect(data.error).toBe("Company not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return metrics data for valid requests", async () => {
|
it("should return dashboard metrics successfully for admin", async () => {
|
||||||
const { getServerSession } = await import("next-auth");
|
const { getServerSession } = await import("next-auth");
|
||||||
const { prisma } = await import("../../lib/prisma");
|
const { prisma } = await import("../../lib/prisma");
|
||||||
|
|
||||||
|
vi.mocked(getServerSession).mockResolvedValue({
|
||||||
|
user: { email: "admin@example.com" },
|
||||||
|
expires: "2024-12-31",
|
||||||
|
});
|
||||||
|
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
id: "user1",
|
id: "user1",
|
||||||
email: "test@example.com",
|
name: "Test Admin",
|
||||||
|
email: "admin@example.com",
|
||||||
companyId: "company1",
|
companyId: "company1",
|
||||||
role: "ADMIN",
|
role: "ADMIN" as const,
|
||||||
password: "hashed",
|
password: "hashed",
|
||||||
|
resetToken: null,
|
||||||
|
resetTokenExpiry: null,
|
||||||
|
invitedAt: null,
|
||||||
|
invitedBy: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
@ -123,7 +131,10 @@ describe("/api/dashboard/metrics", () => {
|
|||||||
id: "company1",
|
id: "company1",
|
||||||
name: "Test Company",
|
name: "Test Company",
|
||||||
csvUrl: "http://example.com/data.csv",
|
csvUrl: "http://example.com/data.csv",
|
||||||
sentimentAlert: 0.5,
|
csvUsername: null,
|
||||||
|
csvPassword: null,
|
||||||
|
dashboardOpts: {},
|
||||||
|
maxUsers: 10,
|
||||||
status: "ACTIVE" as const,
|
status: "ACTIVE" as const,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@ -132,49 +143,32 @@ describe("/api/dashboard/metrics", () => {
|
|||||||
const mockSessions = [
|
const mockSessions = [
|
||||||
{
|
{
|
||||||
id: "session1",
|
id: "session1",
|
||||||
sessionId: "s1",
|
|
||||||
companyId: "company1",
|
companyId: "company1",
|
||||||
|
importId: "import1",
|
||||||
startTime: new Date("2024-01-01T10:00:00Z"),
|
startTime: new Date("2024-01-01T10:00:00Z"),
|
||||||
endTime: new Date("2024-01-01T10:30:00Z"),
|
endTime: new Date("2024-01-01T11:00:00Z"),
|
||||||
sentiment: "POSITIVE",
|
ipAddress: "192.168.1.1",
|
||||||
messagesSent: 5,
|
|
||||||
avgResponseTime: 2.5,
|
|
||||||
tokens: 100,
|
|
||||||
tokensEur: 0.002,
|
|
||||||
language: "en",
|
|
||||||
country: "US",
|
country: "US",
|
||||||
category: "SUPPORT",
|
fullTranscriptUrl: null,
|
||||||
createdAt: new Date(),
|
avgResponseTime: null,
|
||||||
updatedAt: new Date(),
|
initialMsg: null,
|
||||||
},
|
messagesSent: 5,
|
||||||
{
|
escalated: false,
|
||||||
id: "session2",
|
forwardedHr: false,
|
||||||
sessionId: "s2",
|
sentiment: "POSITIVE" as const,
|
||||||
companyId: "company1",
|
category: "SCHEDULE_HOURS" as const,
|
||||||
startTime: new Date("2024-01-02T14:00:00Z"),
|
language: "en",
|
||||||
endTime: new Date("2024-01-02T14:15:00Z"),
|
summary: "Test summary",
|
||||||
sentiment: "NEGATIVE",
|
|
||||||
messagesSent: 3,
|
|
||||||
avgResponseTime: 1.8,
|
|
||||||
tokens: 75,
|
|
||||||
tokensEur: 0.0015,
|
|
||||||
language: "es",
|
|
||||||
country: "ES",
|
|
||||||
category: "BILLING",
|
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
vi.mocked(getServerSession).mockResolvedValue({
|
|
||||||
user: { email: "test@example.com" },
|
|
||||||
expires: "2024-12-31",
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
||||||
vi.mocked(prisma.company.findUnique).mockResolvedValue(mockCompany);
|
vi.mocked(prisma.company.findUnique).mockResolvedValue(mockCompany);
|
||||||
vi.mocked(prisma.session.findMany).mockResolvedValue(mockSessions);
|
vi.mocked(prisma.session.findMany).mockResolvedValue(mockSessions);
|
||||||
vi.mocked(prisma.session.count).mockResolvedValue(2);
|
vi.mocked(prisma.session.count).mockResolvedValue(1);
|
||||||
|
vi.mocked(prisma.sessionQuestion.findMany).mockResolvedValue([]);
|
||||||
|
|
||||||
const request = new NextRequest(
|
const request = new NextRequest(
|
||||||
"http://localhost:3000/api/dashboard/metrics"
|
"http://localhost:3000/api/dashboard/metrics"
|
||||||
@ -183,23 +177,33 @@ describe("/api/dashboard/metrics", () => {
|
|||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
expect(data).toHaveProperty("totalSessions");
|
||||||
expect(data.metrics).toBeDefined();
|
expect(data).toHaveProperty("sentimentDistribution");
|
||||||
expect(data.company).toBeDefined();
|
expect(data).toHaveProperty("countryCounts");
|
||||||
expect(data.metrics.totalSessions).toBe(2);
|
expect(data).toHaveProperty("topQuestions");
|
||||||
expect(data.company.name).toBe("Test Company");
|
expect(data.totalSessions).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle date range filtering", async () => {
|
it("should return dashboard metrics successfully for regular user", async () => {
|
||||||
const { getServerSession } = await import("next-auth");
|
const { getServerSession } = await import("next-auth");
|
||||||
const { prisma } = await import("../../lib/prisma");
|
const { prisma } = await import("../../lib/prisma");
|
||||||
|
|
||||||
|
vi.mocked(getServerSession).mockResolvedValue({
|
||||||
|
user: { email: "user@example.com" },
|
||||||
|
expires: "2024-12-31",
|
||||||
|
});
|
||||||
|
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
id: "user1",
|
id: "user1",
|
||||||
email: "test@example.com",
|
name: "Test User",
|
||||||
|
email: "user@example.com",
|
||||||
companyId: "company1",
|
companyId: "company1",
|
||||||
role: "ADMIN",
|
role: "USER" as const,
|
||||||
password: "hashed",
|
password: "hashed",
|
||||||
|
resetToken: null,
|
||||||
|
resetTokenExpiry: null,
|
||||||
|
invitedAt: null,
|
||||||
|
invitedBy: null,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
@ -208,142 +212,122 @@ describe("/api/dashboard/metrics", () => {
|
|||||||
id: "company1",
|
id: "company1",
|
||||||
name: "Test Company",
|
name: "Test Company",
|
||||||
csvUrl: "http://example.com/data.csv",
|
csvUrl: "http://example.com/data.csv",
|
||||||
sentimentAlert: 0.5,
|
csvUsername: null,
|
||||||
|
csvPassword: null,
|
||||||
|
dashboardOpts: {},
|
||||||
|
maxUsers: 10,
|
||||||
status: "ACTIVE" as const,
|
status: "ACTIVE" as const,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockSessions = [
|
||||||
|
{
|
||||||
|
id: "session1",
|
||||||
|
companyId: "company1",
|
||||||
|
importId: "import1",
|
||||||
|
startTime: new Date("2024-01-01T10:00:00Z"),
|
||||||
|
endTime: new Date("2024-01-01T11:00:00Z"),
|
||||||
|
ipAddress: "192.168.1.1",
|
||||||
|
country: "US",
|
||||||
|
fullTranscriptUrl: null,
|
||||||
|
avgResponseTime: null,
|
||||||
|
initialMsg: null,
|
||||||
|
messagesSent: 5,
|
||||||
|
escalated: false,
|
||||||
|
forwardedHr: false,
|
||||||
|
sentiment: "POSITIVE" as const,
|
||||||
|
category: "SCHEDULE_HOURS" as const,
|
||||||
|
language: "en",
|
||||||
|
summary: "Test summary",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
||||||
|
vi.mocked(prisma.company.findUnique).mockResolvedValue(mockCompany);
|
||||||
|
vi.mocked(prisma.session.findMany).mockResolvedValue(mockSessions);
|
||||||
|
vi.mocked(prisma.session.count).mockResolvedValue(1);
|
||||||
|
vi.mocked(prisma.sessionQuestion.findMany).mockResolvedValue([]);
|
||||||
|
|
||||||
|
const request = new NextRequest(
|
||||||
|
"http://localhost:3000/api/dashboard/metrics"
|
||||||
|
);
|
||||||
|
const response = await GET(request);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
const data = await response.json();
|
||||||
|
expect(data).toHaveProperty("totalSessions");
|
||||||
|
expect(data).toHaveProperty("sentimentDistribution");
|
||||||
|
expect(data).toHaveProperty("countryCounts");
|
||||||
|
expect(data).toHaveProperty("topQuestions");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle date range filters", async () => {
|
||||||
|
const { getServerSession } = await import("next-auth");
|
||||||
|
const { prisma } = await import("../../lib/prisma");
|
||||||
|
|
||||||
vi.mocked(getServerSession).mockResolvedValue({
|
vi.mocked(getServerSession).mockResolvedValue({
|
||||||
user: { email: "test@example.com" },
|
user: { email: "user@example.com" },
|
||||||
expires: "2024-12-31",
|
expires: "2024-12-31",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockUser = {
|
||||||
|
id: "user1",
|
||||||
|
name: "Test User",
|
||||||
|
email: "user@example.com",
|
||||||
|
companyId: "company1",
|
||||||
|
role: "USER" as const,
|
||||||
|
password: "hashed",
|
||||||
|
resetToken: null,
|
||||||
|
resetTokenExpiry: null,
|
||||||
|
invitedAt: null,
|
||||||
|
invitedBy: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockCompany = {
|
||||||
|
id: "company1",
|
||||||
|
name: "Test Company",
|
||||||
|
csvUrl: "http://example.com/data.csv",
|
||||||
|
csvUsername: null,
|
||||||
|
csvPassword: null,
|
||||||
|
dashboardOpts: {},
|
||||||
|
maxUsers: 10,
|
||||||
|
status: "ACTIVE" as const,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
||||||
vi.mocked(prisma.company.findUnique).mockResolvedValue(mockCompany);
|
vi.mocked(prisma.company.findUnique).mockResolvedValue(mockCompany);
|
||||||
vi.mocked(prisma.session.findMany).mockResolvedValue([]);
|
vi.mocked(prisma.session.findMany).mockResolvedValue([]);
|
||||||
vi.mocked(prisma.session.count).mockResolvedValue(0);
|
vi.mocked(prisma.session.count).mockResolvedValue(0);
|
||||||
|
vi.mocked(prisma.sessionQuestion.findMany).mockResolvedValue([]);
|
||||||
|
|
||||||
const request = new NextRequest(
|
const request = new NextRequest(
|
||||||
"http://localhost:3000/api/dashboard/metrics?startDate=2024-01-01&endDate=2024-01-31"
|
"http://localhost:3000/api/dashboard/metrics?startDate=2024-01-01&endDate=2024-01-31"
|
||||||
);
|
);
|
||||||
const response = await GET(request);
|
const response = await GET(request);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(prisma.session.findMany).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
where: expect.objectContaining({
|
|
||||||
companyId: "company1",
|
|
||||||
startTime: expect.objectContaining({
|
|
||||||
gte: expect.any(Date),
|
|
||||||
lte: expect.any(Date),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should calculate metrics correctly", async () => {
|
|
||||||
const { getServerSession } = await import("next-auth");
|
|
||||||
const { prisma } = await import("../../lib/prisma");
|
|
||||||
|
|
||||||
const mockUser = {
|
|
||||||
id: "user1",
|
|
||||||
email: "test@example.com",
|
|
||||||
companyId: "company1",
|
|
||||||
role: "ADMIN",
|
|
||||||
password: "hashed",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockCompany = {
|
|
||||||
id: "company1",
|
|
||||||
name: "Test Company",
|
|
||||||
csvUrl: "http://example.com/data.csv",
|
|
||||||
sentimentAlert: 0.5,
|
|
||||||
status: "ACTIVE" as const,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockSessions = [
|
|
||||||
{
|
|
||||||
id: "session1",
|
|
||||||
sessionId: "s1",
|
|
||||||
companyId: "company1",
|
|
||||||
startTime: new Date("2024-01-01T10:00:00Z"),
|
|
||||||
endTime: new Date("2024-01-01T10:30:00Z"),
|
|
||||||
sentiment: "POSITIVE",
|
|
||||||
messagesSent: 5,
|
|
||||||
avgResponseTime: 2.0,
|
|
||||||
tokens: 100,
|
|
||||||
tokensEur: 0.002,
|
|
||||||
language: "en",
|
|
||||||
country: "US",
|
|
||||||
category: "SUPPORT",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "session2",
|
|
||||||
sessionId: "s2",
|
|
||||||
companyId: "company1",
|
|
||||||
startTime: new Date("2024-01-01T14:00:00Z"),
|
|
||||||
endTime: new Date("2024-01-01T14:20:00Z"),
|
|
||||||
sentiment: "NEGATIVE",
|
|
||||||
messagesSent: 3,
|
|
||||||
avgResponseTime: 3.0,
|
|
||||||
tokens: 150,
|
|
||||||
tokensEur: 0.003,
|
|
||||||
language: "en",
|
|
||||||
country: "US",
|
|
||||||
category: "BILLING",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
vi.mocked(getServerSession).mockResolvedValue({
|
|
||||||
user: { email: "test@example.com" },
|
|
||||||
expires: "2024-12-31",
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
|
||||||
vi.mocked(prisma.company.findUnique).mockResolvedValue(mockCompany);
|
|
||||||
vi.mocked(prisma.session.findMany).mockResolvedValue(mockSessions);
|
|
||||||
vi.mocked(prisma.session.count).mockResolvedValue(2);
|
|
||||||
|
|
||||||
const request = new NextRequest(
|
|
||||||
"http://localhost:3000/api/dashboard/metrics"
|
|
||||||
);
|
|
||||||
const response = await GET(request);
|
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
expect(data.totalSessions).toBe(0);
|
||||||
expect(data.metrics.totalSessions).toBe(2);
|
|
||||||
expect(data.metrics.avgResponseTime).toBe(2.5); // (2.0 + 3.0) / 2
|
|
||||||
expect(data.metrics.totalTokens).toBe(250); // 100 + 150
|
|
||||||
expect(data.metrics.totalTokensEur).toBe(0.005); // 0.002 + 0.003
|
|
||||||
expect(data.metrics.sentimentPositiveCount).toBe(1);
|
|
||||||
expect(data.metrics.sentimentNegativeCount).toBe(1);
|
|
||||||
expect(data.metrics.languages).toEqual({ en: 2 });
|
|
||||||
expect(data.metrics.countries).toEqual({ US: 2 });
|
|
||||||
expect(data.metrics.categories).toEqual({ SUPPORT: 1, BILLING: 1 });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle errors gracefully", async () => {
|
it("should handle database errors gracefully", async () => {
|
||||||
const { getServerSession } = await import("next-auth");
|
const { getServerSession } = await import("next-auth");
|
||||||
const { prisma } = await import("../../lib/prisma");
|
const { prisma } = await import("../../lib/prisma");
|
||||||
|
|
||||||
vi.mocked(getServerSession).mockResolvedValue({
|
vi.mocked(getServerSession).mockResolvedValue({
|
||||||
user: { email: "test@example.com" },
|
user: { email: "user@example.com" },
|
||||||
expires: "2024-12-31",
|
expires: "2024-12-31",
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mocked(prisma.user.findUnique).mockRejectedValue(
|
vi.mocked(prisma.user.findUnique).mockRejectedValue(
|
||||||
new Error("Database error")
|
new Error("Database connection failed")
|
||||||
);
|
);
|
||||||
|
|
||||||
const request = new NextRequest(
|
const request = new NextRequest(
|
||||||
@ -353,57 +337,7 @@ describe("/api/dashboard/metrics", () => {
|
|||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
expect(data.error).toBe("Database error");
|
expect(data.error).toBe("Internal server error");
|
||||||
});
|
|
||||||
|
|
||||||
it("should return empty metrics for companies with no sessions", async () => {
|
|
||||||
const { getServerSession } = await import("next-auth");
|
|
||||||
const { prisma } = await import("../../lib/prisma");
|
|
||||||
|
|
||||||
const mockUser = {
|
|
||||||
id: "user1",
|
|
||||||
email: "test@example.com",
|
|
||||||
companyId: "company1",
|
|
||||||
role: "ADMIN",
|
|
||||||
password: "hashed",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockCompany = {
|
|
||||||
id: "company1",
|
|
||||||
name: "Test Company",
|
|
||||||
csvUrl: "http://example.com/data.csv",
|
|
||||||
sentimentAlert: 0.5,
|
|
||||||
status: "ACTIVE" as const,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.mocked(getServerSession).mockResolvedValue({
|
|
||||||
user: { email: "test@example.com" },
|
|
||||||
expires: "2024-12-31",
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockUser);
|
|
||||||
vi.mocked(prisma.company.findUnique).mockResolvedValue(mockCompany);
|
|
||||||
vi.mocked(prisma.session.findMany).mockResolvedValue([]);
|
|
||||||
vi.mocked(prisma.session.count).mockResolvedValue(0);
|
|
||||||
|
|
||||||
const request = new NextRequest(
|
|
||||||
"http://localhost:3000/api/dashboard/metrics"
|
|
||||||
);
|
|
||||||
const response = await GET(request);
|
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
expect(data.metrics.totalSessions).toBe(0);
|
|
||||||
expect(data.metrics.avgResponseTime).toBe(0);
|
|
||||||
expect(data.metrics.totalTokens).toBe(0);
|
|
||||||
expect(data.metrics.languages).toEqual({});
|
|
||||||
expect(data.metrics.countries).toEqual({});
|
|
||||||
expect(data.metrics.categories).toEqual({});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1,362 +1,149 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
import { ProcessingScheduler } from "../../lib/processingScheduler";
|
import { processUnprocessedSessions, getAIProcessingCosts } from "../../lib/processingScheduler";
|
||||||
|
|
||||||
vi.mock("../../lib/prisma", () => ({
|
vi.mock("../../lib/prisma", () => ({
|
||||||
prisma: new PrismaClient(),
|
prisma: {
|
||||||
}));
|
session: {
|
||||||
|
findMany: vi.fn(),
|
||||||
vi.mock("../../lib/env", () => ({
|
update: vi.fn(),
|
||||||
env: {
|
},
|
||||||
OPENAI_API_KEY: "test-key",
|
aIProcessingRequest: {
|
||||||
PROCESSING_BATCH_SIZE: "10",
|
findMany: vi.fn(),
|
||||||
PROCESSING_INTERVAL_MS: "5000",
|
aggregate: vi.fn(),
|
||||||
|
},
|
||||||
|
sessionProcessingStatus: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("Processing Scheduler", () => {
|
vi.mock("../../lib/schedulerConfig", () => ({
|
||||||
let scheduler: ProcessingScheduler;
|
getSchedulerConfig: () => ({ enabled: true }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("node-fetch", () => ({
|
||||||
|
default: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("Processing Scheduler", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
scheduler = new ProcessingScheduler();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
if (scheduler) {
|
|
||||||
scheduler.stop();
|
|
||||||
}
|
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Scheduler lifecycle", () => {
|
describe("processUnprocessedSessions", () => {
|
||||||
it("should initialize with correct default settings", () => {
|
it("should process sessions needing AI analysis", async () => {
|
||||||
expect(scheduler).toBeDefined();
|
|
||||||
expect(scheduler.isRunning()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should start and stop correctly", async () => {
|
|
||||||
scheduler.start();
|
|
||||||
expect(scheduler.isRunning()).toBe(true);
|
|
||||||
|
|
||||||
scheduler.stop();
|
|
||||||
expect(scheduler.isRunning()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not start multiple times", () => {
|
|
||||||
scheduler.start();
|
|
||||||
const firstStart = scheduler.isRunning();
|
|
||||||
|
|
||||||
scheduler.start(); // Should not start again
|
|
||||||
const secondStart = scheduler.isRunning();
|
|
||||||
|
|
||||||
expect(firstStart).toBe(true);
|
|
||||||
expect(secondStart).toBe(true);
|
|
||||||
|
|
||||||
scheduler.stop();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Processing pipeline stages", () => {
|
|
||||||
it("should process transcript fetch stage", async () => {
|
|
||||||
const mockSessions = [
|
const mockSessions = [
|
||||||
{
|
{
|
||||||
id: "session1",
|
id: "session1",
|
||||||
import: {
|
messages: [
|
||||||
fullTranscriptUrl: "http://example.com/transcript1",
|
{ id: "msg1", content: "Hello", role: "user" },
|
||||||
rawTranscriptContent: null,
|
{ id: "msg2", content: "Hi there", role: "assistant" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { prisma } = await import("../../lib/prisma");
|
||||||
|
vi.mocked(prisma.session.findMany).mockResolvedValue(mockSessions);
|
||||||
|
vi.mocked(prisma.session.update).mockResolvedValue({} as any);
|
||||||
|
|
||||||
|
// Mock fetch for OpenAI API
|
||||||
|
const mockFetch = await import("node-fetch");
|
||||||
|
vi.mocked(mockFetch.default).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
id: "chatcmpl-test",
|
||||||
|
model: "gpt-4o",
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: 100,
|
||||||
|
completion_tokens: 50,
|
||||||
|
total_tokens: 150,
|
||||||
},
|
},
|
||||||
},
|
choices: [
|
||||||
];
|
{
|
||||||
|
message: {
|
||||||
const prismaMock = {
|
content: JSON.stringify({
|
||||||
session: {
|
summary: "Test summary",
|
||||||
findMany: vi.fn().mockResolvedValue(mockSessions),
|
sentiment: "POSITIVE",
|
||||||
update: vi.fn().mockResolvedValue({}),
|
category: "SUPPORT",
|
||||||
},
|
language: "en",
|
||||||
};
|
}),
|
||||||
|
|
||||||
vi.doMock("../../lib/prisma", () => ({
|
|
||||||
prisma: prismaMock,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock fetch for transcript content
|
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
|
||||||
ok: true,
|
|
||||||
text: () => Promise.resolve("Mock transcript content"),
|
|
||||||
});
|
|
||||||
|
|
||||||
await scheduler.processTranscriptFetch();
|
|
||||||
|
|
||||||
expect(prismaMock.session.findMany).toHaveBeenCalled();
|
|
||||||
expect(global.fetch).toHaveBeenCalledWith(
|
|
||||||
"http://example.com/transcript1"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should process AI analysis stage", async () => {
|
|
||||||
const mockSessions = [
|
|
||||||
{
|
|
||||||
id: "session1",
|
|
||||||
transcriptContent: "User: Hello\nAssistant: Hi there!",
|
|
||||||
sentiment: null,
|
|
||||||
summary: null,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const prismaMock = {
|
|
||||||
session: {
|
|
||||||
findMany: vi.fn().mockResolvedValue(mockSessions),
|
|
||||||
update: vi.fn().mockResolvedValue({}),
|
|
||||||
},
|
|
||||||
aIProcessingRequest: {
|
|
||||||
create: vi.fn().mockResolvedValue({ id: "request1" }),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.doMock("../../lib/prisma", () => ({
|
|
||||||
prisma: prismaMock,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock OpenAI API
|
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
|
||||||
ok: true,
|
|
||||||
json: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
message: {
|
|
||||||
content: JSON.stringify({
|
|
||||||
sentiment: "POSITIVE",
|
|
||||||
summary: "Friendly greeting exchange",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
|
||||||
usage: {
|
|
||||||
prompt_tokens: 50,
|
|
||||||
completion_tokens: 20,
|
|
||||||
total_tokens: 70,
|
|
||||||
},
|
},
|
||||||
}),
|
],
|
||||||
});
|
}),
|
||||||
|
} as any);
|
||||||
|
|
||||||
await scheduler.processAIAnalysis();
|
await expect(processUnprocessedSessions(1)).resolves.not.toThrow();
|
||||||
|
|
||||||
expect(prismaMock.session.findMany).toHaveBeenCalled();
|
|
||||||
expect(prismaMock.aIProcessingRequest.create).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle OpenAI API errors gracefully", async () => {
|
it("should handle errors gracefully", async () => {
|
||||||
const mockSessions = [
|
const { prisma } = await import("../../lib/prisma");
|
||||||
{
|
vi.mocked(prisma.session.findMany).mockRejectedValue(new Error("Database error"));
|
||||||
id: "session1",
|
|
||||||
transcriptContent: "User: Hello",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const prismaMock = {
|
await expect(processUnprocessedSessions(1)).resolves.not.toThrow();
|
||||||
session: {
|
|
||||||
findMany: vi.fn().mockResolvedValue(mockSessions),
|
|
||||||
},
|
|
||||||
aIProcessingRequest: {
|
|
||||||
create: vi.fn().mockResolvedValue({ id: "request1" }),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.doMock("../../lib/prisma", () => ({
|
|
||||||
prisma: prismaMock,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock failed OpenAI API call
|
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
|
||||||
ok: false,
|
|
||||||
status: 429,
|
|
||||||
text: () => Promise.resolve("Rate limit exceeded"),
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(scheduler.processAIAnalysis()).rejects.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should process question extraction stage", async () => {
|
|
||||||
const mockSessions = [
|
|
||||||
{
|
|
||||||
id: "session1",
|
|
||||||
transcriptContent:
|
|
||||||
"User: How do I reset my password?\nAssistant: You can reset it in settings.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const prismaMock = {
|
|
||||||
session: {
|
|
||||||
findMany: vi.fn().mockResolvedValue(mockSessions),
|
|
||||||
update: vi.fn().mockResolvedValue({}),
|
|
||||||
},
|
|
||||||
question: {
|
|
||||||
upsert: vi.fn().mockResolvedValue({}),
|
|
||||||
},
|
|
||||||
aIProcessingRequest: {
|
|
||||||
create: vi.fn().mockResolvedValue({ id: "request1" }),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.doMock("../../lib/prisma", () => ({
|
|
||||||
prisma: prismaMock,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock OpenAI API for question extraction
|
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
|
||||||
ok: true,
|
|
||||||
json: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
message: {
|
|
||||||
content: JSON.stringify({
|
|
||||||
questions: ["How do I reset my password?"],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
usage: {
|
|
||||||
prompt_tokens: 30,
|
|
||||||
completion_tokens: 15,
|
|
||||||
total_tokens: 45,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
await scheduler.processQuestionExtraction();
|
|
||||||
|
|
||||||
expect(prismaMock.session.findMany).toHaveBeenCalled();
|
|
||||||
expect(prismaMock.question.upsert).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Error handling", () => {
|
describe("getAIProcessingCosts", () => {
|
||||||
it("should handle database connection errors", async () => {
|
it("should calculate processing costs correctly", async () => {
|
||||||
const prismaMock = {
|
const mockAggregation = {
|
||||||
session: {
|
_sum: {
|
||||||
findMany: vi
|
totalCostEur: 10.50,
|
||||||
.fn()
|
promptTokens: 1000,
|
||||||
.mockRejectedValue(new Error("Database connection failed")),
|
completionTokens: 500,
|
||||||
|
totalTokens: 1500,
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
id: 25,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.doMock("../../lib/prisma", () => ({
|
const { prisma } = await import("../../lib/prisma");
|
||||||
prisma: prismaMock,
|
vi.mocked(prisma.aIProcessingRequest.aggregate).mockResolvedValue(mockAggregation);
|
||||||
}));
|
|
||||||
|
|
||||||
await expect(scheduler.processTranscriptFetch()).rejects.toThrow(
|
const result = await getAIProcessingCosts();
|
||||||
"Database connection failed"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle invalid transcript URLs", async () => {
|
expect(result).toEqual({
|
||||||
const mockSessions = [
|
totalCostEur: 10.50,
|
||||||
{
|
totalRequests: 25,
|
||||||
id: "session1",
|
totalPromptTokens: 1000,
|
||||||
import: {
|
totalCompletionTokens: 500,
|
||||||
fullTranscriptUrl: "invalid-url",
|
totalTokens: 1500,
|
||||||
rawTranscriptContent: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const prismaMock = {
|
|
||||||
session: {
|
|
||||||
findMany: vi.fn().mockResolvedValue(mockSessions),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.doMock("../../lib/prisma", () => ({
|
|
||||||
prisma: prismaMock,
|
|
||||||
}));
|
|
||||||
|
|
||||||
global.fetch = vi.fn().mockRejectedValue(new Error("Invalid URL"));
|
|
||||||
|
|
||||||
await expect(scheduler.processTranscriptFetch()).rejects.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle malformed JSON responses from OpenAI", async () => {
|
|
||||||
const mockSessions = [
|
|
||||||
{
|
|
||||||
id: "session1",
|
|
||||||
transcriptContent: "User: Hello",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const prismaMock = {
|
|
||||||
session: {
|
|
||||||
findMany: vi.fn().mockResolvedValue(mockSessions),
|
|
||||||
},
|
|
||||||
aIProcessingRequest: {
|
|
||||||
create: vi.fn().mockResolvedValue({ id: "request1" }),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.doMock("../../lib/prisma", () => ({
|
|
||||||
prisma: prismaMock,
|
|
||||||
}));
|
|
||||||
|
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
|
||||||
ok: true,
|
|
||||||
json: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
choices: [
|
|
||||||
{
|
|
||||||
message: {
|
|
||||||
content: "Invalid JSON response",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
usage: { total_tokens: 10 },
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
await expect(scheduler.processAIAnalysis()).rejects.toThrow();
|
it("should handle null aggregation results", async () => {
|
||||||
|
const mockAggregation = {
|
||||||
|
_sum: {
|
||||||
|
totalCostEur: null,
|
||||||
|
promptTokens: null,
|
||||||
|
completionTokens: null,
|
||||||
|
totalTokens: null,
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { prisma } = await import("../../lib/prisma");
|
||||||
|
vi.mocked(prisma.aIProcessingRequest.aggregate).mockResolvedValue(mockAggregation);
|
||||||
|
|
||||||
|
const result = await getAIProcessingCosts();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
totalCostEur: 0,
|
||||||
|
totalRequests: 0,
|
||||||
|
totalPromptTokens: 0,
|
||||||
|
totalCompletionTokens: 0,
|
||||||
|
totalTokens: 0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
describe("Rate limiting and batching", () => {
|
|
||||||
it("should respect batch size limits", async () => {
|
|
||||||
const mockSessions = Array.from({ length: 25 }, (_, i) => ({
|
|
||||||
id: `session${i}`,
|
|
||||||
transcriptContent: `Content ${i}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const prismaMock = {
|
|
||||||
session: {
|
|
||||||
findMany: vi.fn().mockResolvedValue(mockSessions),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.doMock("../../lib/prisma", () => ({
|
|
||||||
prisma: prismaMock,
|
|
||||||
}));
|
|
||||||
|
|
||||||
await scheduler.processAIAnalysis();
|
|
||||||
|
|
||||||
// Should only process up to batch size (10 by default)
|
|
||||||
expect(prismaMock.session.findMany).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
take: 10,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle rate limiting gracefully", async () => {
|
|
||||||
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
|
|
||||||
global.fetch = vi.fn().mockResolvedValue({
|
|
||||||
ok: false,
|
|
||||||
status: 429,
|
|
||||||
text: () => Promise.resolve("Rate limit exceeded"),
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(scheduler.processAIAnalysis()).rejects.toThrow();
|
|
||||||
|
|
||||||
consoleSpy.mockRestore();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,193 +1,188 @@
|
|||||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||||
import { parseTranscriptContent } from "../../lib/transcriptParser";
|
import { parseTranscriptToMessages } from "../../lib/transcriptParser";
|
||||||
|
|
||||||
describe("Transcript Parser", () => {
|
describe("Transcript Parser", () => {
|
||||||
|
const startTime = new Date('2024-01-01T10:00:00Z');
|
||||||
|
const endTime = new Date('2024-01-01T10:30:00Z');
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("parseTranscriptContent", () => {
|
describe("parseTranscriptToMessages", () => {
|
||||||
it("should parse basic transcript with timestamps", () => {
|
it("should parse basic transcript with timestamps", () => {
|
||||||
const transcript = `
|
const transcript = `
|
||||||
[10:00:00] User: Hello, I need help with my account
|
[10.01.2024 10:00:00] User: Hello, I need help with my account
|
||||||
[10:00:15] Assistant: I'd be happy to help you with your account. What specific issue are you experiencing?
|
[10.01.2024 10:00:15] Assistant: I'd be happy to help you with your account. What specific issue are you experiencing?
|
||||||
[10:00:45] User: I can't log in to my dashboard
|
[10.01.2024 10:00:45] User: I can't log in to my account
|
||||||
[10:01:00] Assistant: Let me help you troubleshoot that login issue.
|
`;
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const messages = parseTranscriptContent(transcript);
|
const result = parseTranscriptToMessages(transcript, startTime, endTime);
|
||||||
|
|
||||||
expect(messages).toHaveLength(4);
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.messages).toHaveLength(3);
|
||||||
expect(messages[0]).toEqual({
|
expect(result.messages![0].role).toBe("User");
|
||||||
timestamp: new Date("1970-01-01T10:00:00.000Z"),
|
expect(result.messages![0].content).toBe("Hello, I need help with my account");
|
||||||
role: "User",
|
expect(result.messages![1].role).toBe("Assistant");
|
||||||
content: "Hello, I need help with my account",
|
expect(result.messages![2].role).toBe("User");
|
||||||
order: 0,
|
expect(result.messages![2].content).toBe("I can't log in to my account");
|
||||||
});
|
|
||||||
|
|
||||||
expect(messages[1]).toEqual({
|
|
||||||
timestamp: new Date("1970-01-01T10:00:15.000Z"),
|
|
||||||
role: "Assistant",
|
|
||||||
content:
|
|
||||||
"I'd be happy to help you with your account. What specific issue are you experiencing?",
|
|
||||||
order: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(messages[3].order).toBe(3);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle transcript without timestamps", () => {
|
it("should parse transcript without timestamps", () => {
|
||||||
const transcript = `
|
const transcript = `
|
||||||
User: Hello there
|
User: Hello there
|
||||||
Assistant: Hi! How can I help you today?
|
Assistant: Hello! How can I help you today?
|
||||||
User: I need support
|
User: I need support with my order
|
||||||
Assistant: I'm here to help.
|
`;
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const messages = parseTranscriptContent(transcript);
|
const result = parseTranscriptToMessages(transcript, startTime, endTime);
|
||||||
|
|
||||||
expect(messages).toHaveLength(4);
|
expect(result.success).toBe(true);
|
||||||
expect(messages[0].timestamp).toBeNull();
|
expect(result.messages).toHaveLength(3);
|
||||||
expect(messages[0].role).toBe("User");
|
expect(result.messages![0].role).toBe("User");
|
||||||
expect(messages[0].content).toBe("Hello there");
|
expect(result.messages![0].content).toBe("Hello there");
|
||||||
expect(messages[0].order).toBe(0);
|
expect(result.messages![1].role).toBe("Assistant");
|
||||||
|
expect(result.messages![1].content).toBe("Hello! How can I help you today?");
|
||||||
|
expect(result.messages![2].role).toBe("User");
|
||||||
|
expect(result.messages![2].content).toBe("I need support with my order");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multi-line messages", () => {
|
||||||
|
const transcript = `
|
||||||
|
User: I have a complex question
|
||||||
|
about my billing
|
||||||
|
Assistant: I understand your concern.
|
||||||
|
Let me help you with that billing question.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = parseTranscriptToMessages(transcript, startTime, endTime);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.messages).toHaveLength(2);
|
||||||
|
expect(result.messages![0].role).toBe("User");
|
||||||
|
expect(result.messages![0].content).toContain("about my billing");
|
||||||
|
expect(result.messages![1].role).toBe("Assistant");
|
||||||
|
expect(result.messages![1].content).toContain("Let me help you");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle mixed timestamp formats", () => {
|
it("should handle mixed timestamp formats", () => {
|
||||||
const transcript = `
|
const transcript = `
|
||||||
[2024-01-01 10:00:00] User: Hello
|
[10.01.2024 10:00:00] User: First message with timestamp
|
||||||
10:00:15 Assistant: Hi there
|
Assistant: Message without timestamp
|
||||||
[10:00:30] User: How are you?
|
[10.01.2024 10:01:00] User: Another message with timestamp
|
||||||
Assistant: I'm doing well, thanks!
|
`;
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const messages = parseTranscriptContent(transcript);
|
const result = parseTranscriptToMessages(transcript, startTime, endTime);
|
||||||
|
|
||||||
expect(messages).toHaveLength(4);
|
expect(result.success).toBe(true);
|
||||||
expect(messages[0].timestamp).toEqual(
|
expect(result.messages).toHaveLength(3);
|
||||||
new Date("2024-01-01T10:00:00.000Z")
|
expect(result.messages![0].role).toBe("User");
|
||||||
);
|
expect(result.messages![1].role).toBe("Assistant");
|
||||||
expect(messages[1].timestamp).toEqual(
|
expect(result.messages![2].role).toBe("User");
|
||||||
new Date("1970-01-01T10:00:15.000Z")
|
|
||||||
);
|
|
||||||
expect(messages[2].timestamp).toEqual(
|
|
||||||
new Date("1970-01-01T10:00:30.000Z")
|
|
||||||
);
|
|
||||||
expect(messages[3].timestamp).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle various role formats", () => {
|
it("should assign order values correctly", () => {
|
||||||
const transcript = `
|
const transcript = `
|
||||||
Customer: I have a problem
|
User: First
|
||||||
Support Agent: What can I help with?
|
Assistant: Second
|
||||||
USER: My account is locked
|
User: Third
|
||||||
ASSISTANT: Let me check that for you
|
`;
|
||||||
System: Connection established
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const messages = parseTranscriptContent(transcript);
|
const result = parseTranscriptToMessages(transcript, startTime, endTime);
|
||||||
|
|
||||||
expect(messages).toHaveLength(5);
|
expect(result.success).toBe(true);
|
||||||
expect(messages[0].role).toBe("User"); // Customer -> User
|
expect(result.messages).toHaveLength(3);
|
||||||
expect(messages[1].role).toBe("Assistant"); // Support Agent -> Assistant
|
expect(result.messages![0].order).toBe(0);
|
||||||
expect(messages[2].role).toBe("User"); // USER -> User
|
expect(result.messages![1].order).toBe(1);
|
||||||
expect(messages[3].role).toBe("Assistant"); // ASSISTANT -> Assistant
|
expect(result.messages![2].order).toBe(2);
|
||||||
expect(messages[4].role).toBe("System"); // System -> System
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle malformed transcript gracefully", () => {
|
it("should distribute timestamps evenly when no timestamps are present", () => {
|
||||||
const transcript = `
|
const transcript = `
|
||||||
This is not a proper transcript format
|
User: First
|
||||||
No colons here
|
Assistant: Second
|
||||||
: Empty role
|
User: Third
|
||||||
User:
|
`;
|
||||||
: Empty content
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const messages = parseTranscriptContent(transcript);
|
const result = parseTranscriptToMessages(transcript, startTime, endTime);
|
||||||
|
|
||||||
// Should still try to parse what it can
|
expect(result.success).toBe(true);
|
||||||
expect(messages.length).toBeGreaterThanOrEqual(0);
|
expect(result.messages).toHaveLength(3);
|
||||||
|
|
||||||
|
// First message should be at start time
|
||||||
|
expect(result.messages![0].timestamp.getTime()).toBe(startTime.getTime());
|
||||||
|
|
||||||
|
// Last message should be at end time
|
||||||
|
expect(result.messages![2].timestamp.getTime()).toBe(endTime.getTime());
|
||||||
|
|
||||||
|
// Middle message should be between start and end
|
||||||
|
const midTime = result.messages![1].timestamp.getTime();
|
||||||
|
expect(midTime).toBeGreaterThan(startTime.getTime());
|
||||||
|
expect(midTime).toBeLessThan(endTime.getTime());
|
||||||
|
});
|
||||||
|
|
||||||
// Check that all messages have required fields
|
it("should handle empty content", () => {
|
||||||
messages.forEach((message, index) => {
|
expect(parseTranscriptToMessages("", startTime, endTime)).toEqual({
|
||||||
expect(message).toHaveProperty("role");
|
success: false,
|
||||||
expect(message).toHaveProperty("content");
|
error: "Empty transcript content"
|
||||||
expect(message).toHaveProperty("order", index);
|
});
|
||||||
expect(message).toHaveProperty("timestamp");
|
expect(parseTranscriptToMessages(" \n\n ", startTime, endTime)).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: "Empty transcript content"
|
||||||
|
});
|
||||||
|
expect(parseTranscriptToMessages("\t\r\n", startTime, endTime)).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: "Empty transcript content"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should preserve message order correctly", () => {
|
it("should handle content with no valid messages", () => {
|
||||||
const transcript = `
|
const transcript = `
|
||||||
User: First message
|
Just some random text
|
||||||
Assistant: Second message
|
without any proper format
|
||||||
User: Third message
|
`;
|
||||||
Assistant: Fourth message
|
|
||||||
User: Fifth message
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const messages = parseTranscriptContent(transcript);
|
const result = parseTranscriptToMessages(transcript, startTime, endTime);
|
||||||
|
|
||||||
expect(messages).toHaveLength(5);
|
expect(result.success).toBe(false);
|
||||||
messages.forEach((message, index) => {
|
expect(result.error).toBe("No messages found in transcript");
|
||||||
expect(message.order).toBe(index);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle empty or whitespace-only transcript", () => {
|
it("should handle case-insensitive roles", () => {
|
||||||
expect(parseTranscriptContent("")).toEqual([]);
|
|
||||||
expect(parseTranscriptContent(" \n\n ")).toEqual([]);
|
|
||||||
expect(parseTranscriptContent("\t\r\n")).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle special characters in content", () => {
|
|
||||||
const transcript = `
|
const transcript = `
|
||||||
User: Hello! How are you? 😊
|
user: Lower case user
|
||||||
Assistant: I'm great! Thanks for asking. 🤖
|
ASSISTANT: Upper case assistant
|
||||||
User: Can you help with this: https://example.com/issue?id=123&type=urgent
|
System: Mixed case system
|
||||||
Assistant: Absolutely! I'll check that URL for you.
|
`;
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const messages = parseTranscriptContent(transcript);
|
const result = parseTranscriptToMessages(transcript, startTime, endTime);
|
||||||
|
|
||||||
expect(messages).toHaveLength(4);
|
expect(result.success).toBe(true);
|
||||||
expect(messages[0].content).toBe("Hello! How are you? 😊");
|
expect(result.messages).toHaveLength(3);
|
||||||
expect(messages[2].content).toBe(
|
expect(result.messages![0].role).toBe("User");
|
||||||
"Can you help with this: https://example.com/issue?id=123&type=urgent"
|
expect(result.messages![1].role).toBe("Assistant");
|
||||||
);
|
expect(result.messages![2].role).toBe("System");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should normalize role names consistently", () => {
|
it("should parse European date format correctly", () => {
|
||||||
const transcript = `
|
const transcript = `
|
||||||
customer: Hello
|
[10.01.2024 14:30:45] User: Message with European date format
|
||||||
support: Hi there
|
[10.01.2024 14:31:00] Assistant: Response message
|
||||||
CUSTOMER: How are you?
|
`;
|
||||||
SUPPORT: Good thanks
|
|
||||||
Client: Great
|
|
||||||
Agent: Wonderful
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
const messages = parseTranscriptContent(transcript);
|
const result = parseTranscriptToMessages(transcript, startTime, endTime);
|
||||||
|
|
||||||
expect(messages[0].role).toBe("User");
|
expect(result.success).toBe(true);
|
||||||
expect(messages[1].role).toBe("Assistant");
|
expect(result.messages).toHaveLength(2);
|
||||||
expect(messages[2].role).toBe("User");
|
|
||||||
expect(messages[3].role).toBe("Assistant");
|
// Check that timestamps were parsed correctly
|
||||||
expect(messages[4].role).toBe("User");
|
const firstTimestamp = result.messages![0].timestamp;
|
||||||
expect(messages[5].role).toBe("Assistant");
|
expect(firstTimestamp.getFullYear()).toBe(2024);
|
||||||
});
|
expect(firstTimestamp.getMonth()).toBe(0); // January (0-indexed)
|
||||||
|
expect(firstTimestamp.getDate()).toBe(10);
|
||||||
it("should handle long content without truncation", () => {
|
expect(firstTimestamp.getHours()).toBe(14);
|
||||||
const longContent = "A".repeat(5000);
|
expect(firstTimestamp.getMinutes()).toBe(30);
|
||||||
const transcript = `User: ${longContent}`;
|
expect(firstTimestamp.getSeconds()).toBe(45);
|
||||||
|
|
||||||
const messages = parseTranscriptContent(transcript);
|
|
||||||
|
|
||||||
expect(messages).toHaveLength(1);
|
|
||||||
expect(messages[0].content).toBe(longContent);
|
|
||||||
expect(messages[0].content.length).toBe(5000);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user