mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 20:12:08 +01:00
feat: implement comprehensive email system with rate limiting and extensive test suite
- Add robust email service with rate limiting and configuration management - Implement shared rate limiter utility for consistent API protection - Create comprehensive test suite for core processing pipeline - Add API tests for dashboard metrics and authentication routes - Fix date range picker infinite loop issue - Improve session lookup in refresh sessions API - Refactor session API routing with better code organization - Update processing pipeline status monitoring - Clean up leftover files and improve code formatting
This commit is contained in:
229
tests/lib/importProcessor.test.ts
Normal file
229
tests/lib/importProcessor.test.ts
Normal file
@ -0,0 +1,229 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { processQueuedImports } from "../../lib/importProcessor";
|
||||
import { ProcessingStatusManager } from "../../lib/processingStatusManager";
|
||||
|
||||
vi.mock("../../lib/prisma", () => ({
|
||||
prisma: new PrismaClient(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/processingStatusManager", () => ({
|
||||
ProcessingStatusManager: {
|
||||
initializeStage: vi.fn(),
|
||||
startStage: vi.fn(),
|
||||
completeStage: vi.fn(),
|
||||
failStage: vi.fn(),
|
||||
skipStage: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Import Processor", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("processQueuedImports", () => {
|
||||
it("should process imports within specified limit", async () => {
|
||||
const mockSessionImports = [
|
||||
{
|
||||
id: "import1",
|
||||
companyId: "company1",
|
||||
externalSessionId: "session1",
|
||||
startTimeRaw: "2024-01-01 10:00:00",
|
||||
endTimeRaw: "2024-01-01 11:00:00",
|
||||
ipAddress: "192.168.1.1",
|
||||
countryCode: "US",
|
||||
language: "en",
|
||||
messagesSent: 5,
|
||||
sentimentRaw: "positive",
|
||||
escalatedRaw: "false",
|
||||
forwardedHrRaw: "false",
|
||||
fullTranscriptUrl: "http://example.com/transcript1",
|
||||
avgResponseTimeSeconds: 2.5,
|
||||
tokens: 100,
|
||||
tokensEur: 0.002,
|
||||
category: "SUPPORT",
|
||||
initialMessage: "Hello, I need help",
|
||||
},
|
||||
];
|
||||
|
||||
// Mock the prisma queries
|
||||
const prismaMock = {
|
||||
sessionImport: {
|
||||
findMany: vi.fn().mockResolvedValue(mockSessionImports),
|
||||
},
|
||||
session: {
|
||||
create: vi.fn().mockResolvedValue({
|
||||
id: "new-session-id",
|
||||
companyId: "company1",
|
||||
sessionId: "session1",
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// Replace the prisma import with our mock
|
||||
vi.doMock("../../lib/prisma", () => ({
|
||||
prisma: prismaMock,
|
||||
}));
|
||||
|
||||
const result = await processQueuedImports(10);
|
||||
|
||||
expect(prismaMock.sessionImport.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
processingStatus: {
|
||||
some: {
|
||||
stage: "CSV_IMPORT",
|
||||
status: "COMPLETED",
|
||||
},
|
||||
none: {
|
||||
stage: "SESSION_CREATION",
|
||||
status: "COMPLETED",
|
||||
},
|
||||
},
|
||||
},
|
||||
take: 10,
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
|
||||
expect(result.processed).toBe(1);
|
||||
expect(result.total).toBe(1);
|
||||
});
|
||||
|
||||
it("should handle processing errors gracefully", async () => {
|
||||
const mockSessionImports = [
|
||||
{
|
||||
id: "import1",
|
||||
companyId: "company1",
|
||||
externalSessionId: "session1",
|
||||
startTimeRaw: "invalid-date",
|
||||
endTimeRaw: "2024-01-01 11:00:00",
|
||||
},
|
||||
];
|
||||
|
||||
const prismaMock = {
|
||||
sessionImport: {
|
||||
findMany: vi.fn().mockResolvedValue(mockSessionImports),
|
||||
},
|
||||
session: {
|
||||
create: vi.fn().mockRejectedValue(new Error("Database error")),
|
||||
},
|
||||
};
|
||||
|
||||
vi.doMock("../../lib/prisma", () => ({
|
||||
prisma: prismaMock,
|
||||
}));
|
||||
|
||||
const result = await processQueuedImports(10);
|
||||
|
||||
expect(ProcessingStatusManager.failStage).toHaveBeenCalled();
|
||||
expect(result.processed).toBe(0);
|
||||
expect(result.errors).toBe(1);
|
||||
});
|
||||
|
||||
it("should correctly parse sentiment values", async () => {
|
||||
const testCases = [
|
||||
{ sentimentRaw: "positive", expected: "POSITIVE" },
|
||||
{ sentimentRaw: "negative", expected: "NEGATIVE" },
|
||||
{ sentimentRaw: "neutral", expected: "NEUTRAL" },
|
||||
{ sentimentRaw: "unknown", expected: "NEUTRAL" },
|
||||
{ sentimentRaw: null, expected: "NEUTRAL" },
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const mockImport = {
|
||||
id: "import1",
|
||||
companyId: "company1",
|
||||
externalSessionId: "session1",
|
||||
sentimentRaw: testCase.sentimentRaw,
|
||||
startTimeRaw: "2024-01-01 10:00:00",
|
||||
endTimeRaw: "2024-01-01 11:00:00",
|
||||
};
|
||||
|
||||
const prismaMock = {
|
||||
sessionImport: {
|
||||
findMany: vi.fn().mockResolvedValue([mockImport]),
|
||||
},
|
||||
session: {
|
||||
create: vi.fn().mockImplementation((data) => {
|
||||
expect(data.data.sentiment).toBe(testCase.expected);
|
||||
return Promise.resolve({ id: "session-id" });
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
vi.doMock("../../lib/prisma", () => ({
|
||||
prisma: prismaMock,
|
||||
}));
|
||||
|
||||
await processQueuedImports(1);
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle boolean string conversions", async () => {
|
||||
const mockImport = {
|
||||
id: "import1",
|
||||
companyId: "company1",
|
||||
externalSessionId: "session1",
|
||||
escalatedRaw: "true",
|
||||
forwardedHrRaw: "false",
|
||||
startTimeRaw: "2024-01-01 10:00:00",
|
||||
endTimeRaw: "2024-01-01 11:00:00",
|
||||
};
|
||||
|
||||
const prismaMock = {
|
||||
sessionImport: {
|
||||
findMany: vi.fn().mockResolvedValue([mockImport]),
|
||||
},
|
||||
session: {
|
||||
create: vi.fn().mockImplementation((data) => {
|
||||
expect(data.data.escalated).toBe(true);
|
||||
expect(data.data.forwardedHr).toBe(false);
|
||||
return Promise.resolve({ id: "session-id" });
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
vi.doMock("../../lib/prisma", () => ({
|
||||
prisma: prismaMock,
|
||||
}));
|
||||
|
||||
await processQueuedImports(1);
|
||||
});
|
||||
|
||||
it("should validate required fields", async () => {
|
||||
const mockImport = {
|
||||
id: "import1",
|
||||
companyId: null, // Invalid - missing required field
|
||||
externalSessionId: "session1",
|
||||
startTimeRaw: "2024-01-01 10:00:00",
|
||||
endTimeRaw: "2024-01-01 11:00:00",
|
||||
};
|
||||
|
||||
const prismaMock = {
|
||||
sessionImport: {
|
||||
findMany: vi.fn().mockResolvedValue([mockImport]),
|
||||
},
|
||||
session: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
vi.doMock("../../lib/prisma", () => ({
|
||||
prisma: prismaMock,
|
||||
}));
|
||||
|
||||
const result = await processQueuedImports(1);
|
||||
|
||||
expect(ProcessingStatusManager.failStage).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
"SESSION_CREATION",
|
||||
expect.stringContaining("Missing required field")
|
||||
);
|
||||
expect(result.errors).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
362
tests/lib/processingScheduler.test.ts
Normal file
362
tests/lib/processingScheduler.test.ts
Normal file
@ -0,0 +1,362 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { ProcessingScheduler } from "../../lib/processingScheduler";
|
||||
|
||||
vi.mock("../../lib/prisma", () => ({
|
||||
prisma: new PrismaClient(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/env", () => ({
|
||||
env: {
|
||||
OPENAI_API_KEY: "test-key",
|
||||
PROCESSING_BATCH_SIZE: "10",
|
||||
PROCESSING_INTERVAL_MS: "5000",
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Processing Scheduler", () => {
|
||||
let scheduler: ProcessingScheduler;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
scheduler = new ProcessingScheduler();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (scheduler) {
|
||||
scheduler.stop();
|
||||
}
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("Scheduler lifecycle", () => {
|
||||
it("should initialize with correct default settings", () => {
|
||||
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 = [
|
||||
{
|
||||
id: "session1",
|
||||
import: {
|
||||
fullTranscriptUrl: "http://example.com/transcript1",
|
||||
rawTranscriptContent: null,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const prismaMock = {
|
||||
session: {
|
||||
findMany: vi.fn().mockResolvedValue(mockSessions),
|
||||
update: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await scheduler.processAIAnalysis();
|
||||
|
||||
expect(prismaMock.session.findMany).toHaveBeenCalled();
|
||||
expect(prismaMock.aIProcessingRequest.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle OpenAI API errors gracefully", 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,
|
||||
}));
|
||||
|
||||
// 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", () => {
|
||||
it("should handle database connection errors", async () => {
|
||||
const prismaMock = {
|
||||
session: {
|
||||
findMany: vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("Database connection failed")),
|
||||
},
|
||||
};
|
||||
|
||||
vi.doMock("../../lib/prisma", () => ({
|
||||
prisma: prismaMock,
|
||||
}));
|
||||
|
||||
await expect(scheduler.processTranscriptFetch()).rejects.toThrow(
|
||||
"Database connection failed"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle invalid transcript URLs", async () => {
|
||||
const mockSessions = [
|
||||
{
|
||||
id: "session1",
|
||||
import: {
|
||||
fullTranscriptUrl: "invalid-url",
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
193
tests/lib/transcriptParser.test.ts
Normal file
193
tests/lib/transcriptParser.test.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { parseTranscriptContent } from "../../lib/transcriptParser";
|
||||
|
||||
describe("Transcript Parser", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("parseTranscriptContent", () => {
|
||||
it("should parse basic transcript with timestamps", () => {
|
||||
const transcript = `
|
||||
[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:00:45] User: I can't log in to my dashboard
|
||||
[10:01:00] Assistant: Let me help you troubleshoot that login issue.
|
||||
`.trim();
|
||||
|
||||
const messages = parseTranscriptContent(transcript);
|
||||
|
||||
expect(messages).toHaveLength(4);
|
||||
|
||||
expect(messages[0]).toEqual({
|
||||
timestamp: new Date("1970-01-01T10:00:00.000Z"),
|
||||
role: "User",
|
||||
content: "Hello, I need help with my account",
|
||||
order: 0,
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const transcript = `
|
||||
User: Hello there
|
||||
Assistant: Hi! How can I help you today?
|
||||
User: I need support
|
||||
Assistant: I'm here to help.
|
||||
`.trim();
|
||||
|
||||
const messages = parseTranscriptContent(transcript);
|
||||
|
||||
expect(messages).toHaveLength(4);
|
||||
expect(messages[0].timestamp).toBeNull();
|
||||
expect(messages[0].role).toBe("User");
|
||||
expect(messages[0].content).toBe("Hello there");
|
||||
expect(messages[0].order).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle mixed timestamp formats", () => {
|
||||
const transcript = `
|
||||
[2024-01-01 10:00:00] User: Hello
|
||||
10:00:15 Assistant: Hi there
|
||||
[10:00:30] User: How are you?
|
||||
Assistant: I'm doing well, thanks!
|
||||
`.trim();
|
||||
|
||||
const messages = parseTranscriptContent(transcript);
|
||||
|
||||
expect(messages).toHaveLength(4);
|
||||
expect(messages[0].timestamp).toEqual(
|
||||
new Date("2024-01-01T10:00:00.000Z")
|
||||
);
|
||||
expect(messages[1].timestamp).toEqual(
|
||||
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", () => {
|
||||
const transcript = `
|
||||
Customer: I have a problem
|
||||
Support Agent: What can I help with?
|
||||
USER: My account is locked
|
||||
ASSISTANT: Let me check that for you
|
||||
System: Connection established
|
||||
`.trim();
|
||||
|
||||
const messages = parseTranscriptContent(transcript);
|
||||
|
||||
expect(messages).toHaveLength(5);
|
||||
expect(messages[0].role).toBe("User"); // Customer -> User
|
||||
expect(messages[1].role).toBe("Assistant"); // Support Agent -> Assistant
|
||||
expect(messages[2].role).toBe("User"); // USER -> User
|
||||
expect(messages[3].role).toBe("Assistant"); // ASSISTANT -> Assistant
|
||||
expect(messages[4].role).toBe("System"); // System -> System
|
||||
});
|
||||
|
||||
it("should handle malformed transcript gracefully", () => {
|
||||
const transcript = `
|
||||
This is not a proper transcript format
|
||||
No colons here
|
||||
: Empty role
|
||||
User:
|
||||
: Empty content
|
||||
`.trim();
|
||||
|
||||
const messages = parseTranscriptContent(transcript);
|
||||
|
||||
// Should still try to parse what it can
|
||||
expect(messages.length).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Check that all messages have required fields
|
||||
messages.forEach((message, index) => {
|
||||
expect(message).toHaveProperty("role");
|
||||
expect(message).toHaveProperty("content");
|
||||
expect(message).toHaveProperty("order", index);
|
||||
expect(message).toHaveProperty("timestamp");
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve message order correctly", () => {
|
||||
const transcript = `
|
||||
User: First message
|
||||
Assistant: Second message
|
||||
User: Third message
|
||||
Assistant: Fourth message
|
||||
User: Fifth message
|
||||
`.trim();
|
||||
|
||||
const messages = parseTranscriptContent(transcript);
|
||||
|
||||
expect(messages).toHaveLength(5);
|
||||
messages.forEach((message, index) => {
|
||||
expect(message.order).toBe(index);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle empty or whitespace-only transcript", () => {
|
||||
expect(parseTranscriptContent("")).toEqual([]);
|
||||
expect(parseTranscriptContent(" \n\n ")).toEqual([]);
|
||||
expect(parseTranscriptContent("\t\r\n")).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle special characters in content", () => {
|
||||
const transcript = `
|
||||
User: Hello! How are you? 😊
|
||||
Assistant: I'm great! Thanks for asking. 🤖
|
||||
User: Can you help with this: https://example.com/issue?id=123&type=urgent
|
||||
Assistant: Absolutely! I'll check that URL for you.
|
||||
`.trim();
|
||||
|
||||
const messages = parseTranscriptContent(transcript);
|
||||
|
||||
expect(messages).toHaveLength(4);
|
||||
expect(messages[0].content).toBe("Hello! How are you? 😊");
|
||||
expect(messages[2].content).toBe(
|
||||
"Can you help with this: https://example.com/issue?id=123&type=urgent"
|
||||
);
|
||||
});
|
||||
|
||||
it("should normalize role names consistently", () => {
|
||||
const transcript = `
|
||||
customer: Hello
|
||||
support: Hi there
|
||||
CUSTOMER: How are you?
|
||||
SUPPORT: Good thanks
|
||||
Client: Great
|
||||
Agent: Wonderful
|
||||
`.trim();
|
||||
|
||||
const messages = parseTranscriptContent(transcript);
|
||||
|
||||
expect(messages[0].role).toBe("User");
|
||||
expect(messages[1].role).toBe("Assistant");
|
||||
expect(messages[2].role).toBe("User");
|
||||
expect(messages[3].role).toBe("Assistant");
|
||||
expect(messages[4].role).toBe("User");
|
||||
expect(messages[5].role).toBe("Assistant");
|
||||
});
|
||||
|
||||
it("should handle long content without truncation", () => {
|
||||
const longContent = "A".repeat(5000);
|
||||
const transcript = `User: ${longContent}`;
|
||||
|
||||
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